Full Code of OnlyInAmerica/BLEMeshChat for AI

master d6dbf0c3c6ed cached
87 files
218.6 KB
51.2k tokens
352 symbols
1 requests
Download .txt
Showing preview only (244K chars total). Download the full file or copy to clipboard to get everything.
Repository: OnlyInAmerica/BLEMeshChat
Branch: master
Commit: d6dbf0c3c6ed
Files: 87
Total size: 218.6 KB

Directory structure:
gitextract_ophh8epy/

├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE.txt
├── README.md
├── android-wait-for-emulator.sh
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── libs/
│   │   └── kalium-jni-1.0.2.jar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── pro/
│       │           └── dbro/
│       │               └── ble/
│       │                   ├── ChatAppTest.java
│       │                   └── util/
│       │                       └── RandomString.java
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   ├── com/
│           │   │   └── google/
│           │   │       └── samples/
│           │   │           └── apps/
│           │   │               └── iosched/
│           │   │                   └── ui/
│           │   │                       └── widget/
│           │   │                           └── ScrimInsetsScrollView.java
│           │   ├── im/
│           │   │   └── delight/
│           │   │       └── android/
│           │   │           └── identicons/
│           │   │               ├── AsymmetricIdenticon.java
│           │   │               ├── Identicon.java
│           │   │               └── SymmetricIdenticon.java
│           │   └── pro/
│           │       └── dbro/
│           │           └── ble/
│           │               ├── ActivityRecevingMessagesIndicator.java
│           │               ├── ChatApp.java
│           │               ├── ChatClient.java
│           │               ├── ChatPeerFlow.java
│           │               ├── PrefsManager.java
│           │               ├── crypto/
│           │               │   ├── KeyPair.java
│           │               │   └── SodiumShaker.java
│           │               ├── data/
│           │               │   ├── ContentProviderStore.java
│           │               │   ├── DataStore.java
│           │               │   └── model/
│           │               │       ├── ChatContentProvider.java
│           │               │       ├── ChatDatabase.java
│           │               │       ├── CursorModel.java
│           │               │       ├── DataUtil.java
│           │               │       ├── IdentityDeliveryTable.java
│           │               │       ├── Message.java
│           │               │       ├── MessageCollection.java
│           │               │       ├── MessageDeliveryTable.java
│           │               │       ├── MessageTable.java
│           │               │       ├── Peer.java
│           │               │       └── PeerTable.java
│           │               ├── protocol/
│           │               │   ├── BLEProtocol.java
│           │               │   ├── IdentityPacket.java
│           │               │   ├── MessagePacket.java
│           │               │   ├── NoDataPacket.java
│           │               │   ├── OwnedIdentityPacket.java
│           │               │   └── Protocol.java
│           │               └── ui/
│           │                   ├── Notification.java
│           │                   ├── activities/
│           │                   │   ├── LogConsumer.java
│           │                   │   ├── MainActivity.java
│           │                   │   └── Util.java
│           │                   ├── adapter/
│           │                   │   ├── CursorFilter.java
│           │                   │   ├── MessageAdapter.java
│           │                   │   ├── PeerAdapter.java
│           │                   │   ├── RecyclerViewCursorAdapter.java
│           │                   │   └── StatusArrayAdapter.java
│           │                   └── fragment/
│           │                       ├── MessagingFragment.java
│           │                       ├── ProfileFragment.java
│           │                       └── WelcomeFragment.java
│           └── res/
│               ├── drawable/
│               │   ├── status_always_online.xml
│               │   ├── status_offline.xml
│               │   ├── status_online_in_foreground.xml
│               │   └── transparent_button.xml
│               ├── layout/
│               │   ├── activity_main.xml
│               │   ├── dialog_welcome.xml
│               │   ├── fragment_message.xml
│               │   ├── fragment_peer.xml
│               │   ├── fragment_peer_profile.xml
│               │   ├── fragment_welcome.xml
│               │   ├── message_item.xml
│               │   ├── peer_item.xml
│               │   └── status_item.xml
│               ├── menu/
│               │   ├── menu_debug.xml
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── attrs.xml
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── ids.xml
│               │   ├── ints.xml
│               │   ├── strings-machine.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── pull_on_app_database.sh
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.crashes
.gradle
/local.properties
/.idea/
.DS_Store
/build
*.iml



================================================
FILE: .gitmodules
================================================
[submodule "submodules/airshare"]
	path = submodules/airshare
	url = https://github.com/OnlyInAmerica/AirShare-Android.git


================================================
FILE: .travis.yml
================================================
language: android
android:
  update-sdk: true
  components:
    - platform-tools
    - build-tools-22.0.1
    - android-22
    - extra-android-m2repository
    - sys-img-armeabi-v7a-android-22

#before_script:
#  - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a
#  - emulator -avd test -no-skin -no-audio -no-window &
#  - bash ./android-wait-for-emulator.sh
#  - adb shell input keyevent 82 &

script:
  - ./gradlew assembleDebug
  # Currently only builds, does not run tests.
  # Enable testing with './gradlew connectedCheck'
  # once I figure out this emulator business


================================================
FILE: LICENSE.txt
================================================
Mozilla Public License, version 2.0

1. Definitions

1.1. "Contributor"

     means each individual or legal entity that creates, contributes to the
     creation of, or owns Covered Software.

1.2. "Contributor Version"

     means the combination of the Contributions of others (if any) used by a
     Contributor and that particular Contributor's Contribution.

1.3. "Contribution"

     means Covered Software of a particular Contributor.

1.4. "Covered Software"

     means Source Code Form to which the initial Contributor has attached the
     notice in Exhibit A, the Executable Form of such Source Code Form, and
     Modifications of such Source Code Form, in each case including portions
     thereof.

1.5. "Incompatible With Secondary Licenses"
     means

     a. that the initial Contributor has attached the notice described in
        Exhibit B to the Covered Software; or

     b. that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the terms of
        a Secondary License.

1.6. "Executable Form"

     means any form of the work other than Source Code Form.

1.7. "Larger Work"

     means a work that combines Covered Software with other material, in a
     separate file or files, that is not Covered Software.

1.8. "License"

     means this document.

1.9. "Licensable"

     means having the right to grant, to the maximum extent possible, whether
     at the time of the initial grant or subsequently, any and all of the
     rights conveyed by this License.

1.10. "Modifications"

     means any of the following:

     a. any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered Software; or

     b. any new file in Source Code Form that contains any Covered Software.

1.11. "Patent Claims" of a Contributor

      means any patent claim(s), including without limitation, method,
      process, and apparatus claims, in any patent Licensable by such
      Contributor that would be infringed, but for the grant of the License,
      by the making, using, selling, offering for sale, having made, import,
      or transfer of either its Contributions or its Contributor Version.

1.12. "Secondary License"

      means either the GNU General Public License, Version 2.0, the GNU Lesser
      General Public License, Version 2.1, the GNU Affero General Public
      License, Version 3.0, or any later versions of those licenses.

1.13. "Source Code Form"

      means the form of the work preferred for making modifications.

1.14. "You" (or "Your")

      means an individual or a legal entity exercising rights under this
      License. For legal entities, "You" includes any entity that controls, is
      controlled by, or is under common control with You. For purposes of this
      definition, "control" means (a) the power, direct or indirect, to cause
      the direction or management of such entity, whether by contract or
      otherwise, or (b) ownership of more than fifty percent (50%) of the
      outstanding shares or beneficial ownership of such entity.


2. License Grants and Conditions

2.1. Grants

     Each Contributor hereby grants You a world-wide, royalty-free,
     non-exclusive license:

     a. under intellectual property rights (other than patent or trademark)
        Licensable by such Contributor to use, reproduce, make available,
        modify, display, perform, distribute, and otherwise exploit its
        Contributions, either on an unmodified basis, with Modifications, or
        as part of a Larger Work; and

     b. under Patent Claims of such Contributor to make, use, sell, offer for
        sale, have made, import, and otherwise transfer either its
        Contributions or its Contributor Version.

2.2. Effective Date

     The licenses granted in Section 2.1 with respect to any Contribution
     become effective for each Contribution on the date the Contributor first
     distributes such Contribution.

2.3. Limitations on Grant Scope

     The licenses granted in this Section 2 are the only rights granted under
     this License. No additional rights or licenses will be implied from the
     distribution or licensing of Covered Software under this License.
     Notwithstanding Section 2.1(b) above, no patent license is granted by a
     Contributor:

     a. for any code that a Contributor has removed from Covered Software; or

     b. for infringements caused by: (i) Your and any other third party's
        modifications of Covered Software, or (ii) the combination of its
        Contributions with other software (except as part of its Contributor
        Version); or

     c. under Patent Claims infringed by Covered Software in the absence of
        its Contributions.

     This License does not grant any rights in the trademarks, service marks,
     or logos of any Contributor (except as may be necessary to comply with
     the notice requirements in Section 3.4).

2.4. Subsequent Licenses

     No Contributor makes additional grants as a result of Your choice to
     distribute the Covered Software under a subsequent version of this
     License (see Section 10.2) or under the terms of a Secondary License (if
     permitted under the terms of Section 3.3).

2.5. Representation

     Each Contributor represents that the Contributor believes its
     Contributions are its original creation(s) or it has sufficient rights to
     grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

     This License is not intended to limit any rights You have under
     applicable copyright doctrines of fair use, fair dealing, or other
     equivalents.

2.7. Conditions

     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
     Section 2.1.


3. Responsibilities

3.1. Distribution of Source Form

     All distribution of Covered Software in Source Code Form, including any
     Modifications that You create or to which You contribute, must be under
     the terms of this License. You must inform recipients that the Source
     Code Form of the Covered Software is governed by the terms of this
     License, and how they can obtain a copy of this License. You may not
     attempt to alter or restrict the recipients' rights in the Source Code
     Form.

3.2. Distribution of Executable Form

     If You distribute Covered Software in Executable Form then:

     a. such Covered Software must also be made available in Source Code Form,
        as described in Section 3.1, and You must inform recipients of the
        Executable Form how they can obtain a copy of such Source Code Form by
        reasonable means in a timely manner, at a charge no more than the cost
        of distribution to the recipient; and

     b. You may distribute such Executable Form under the terms of this
        License, or sublicense it under different terms, provided that the
        license for the Executable Form does not attempt to limit or alter the
        recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

     You may create and distribute a Larger Work under terms of Your choice,
     provided that You also comply with the requirements of this License for
     the Covered Software. If the Larger Work is a combination of Covered
     Software with a work governed by one or more Secondary Licenses, and the
     Covered Software is not Incompatible With Secondary Licenses, this
     License permits You to additionally distribute such Covered Software
     under the terms of such Secondary License(s), so that the recipient of
     the Larger Work may, at their option, further distribute the Covered
     Software under the terms of either this License or such Secondary
     License(s).

3.4. Notices

     You may not remove or alter the substance of any license notices
     (including copyright notices, patent notices, disclaimers of warranty, or
     limitations of liability) contained within the Source Code Form of the
     Covered Software, except that You may alter any license notices to the
     extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

     You may choose to offer, and to charge a fee for, warranty, support,
     indemnity or liability obligations to one or more recipients of Covered
     Software. However, You may do so only on Your own behalf, and not on
     behalf of any Contributor. You must make it absolutely clear that any
     such warranty, support, indemnity, or liability obligation is offered by
     You alone, and You hereby agree to indemnify every Contributor for any
     liability incurred by such Contributor as a result of warranty, support,
     indemnity or liability terms You offer. You may include additional
     disclaimers of warranty and limitations of liability specific to any
     jurisdiction.

4. Inability to Comply Due to Statute or Regulation

   If it is impossible for You to comply with any of the terms of this License
   with respect to some or all of the Covered Software due to statute,
   judicial order, or regulation then You must: (a) comply with the terms of
   this License to the maximum extent possible; and (b) describe the
   limitations and the code they affect. Such description must be placed in a
   text file included with all distributions of the Covered Software under
   this License. Except to the extent prohibited by statute or regulation,
   such description must be sufficiently detailed for a recipient of ordinary
   skill to be able to understand it.

5. Termination

5.1. The rights granted under this License will terminate automatically if You
     fail to comply with any of its terms. However, if You become compliant,
     then the rights granted under this License from a particular Contributor
     are reinstated (a) provisionally, unless and until such Contributor
     explicitly and finally terminates Your grants, and (b) on an ongoing
     basis, if such Contributor fails to notify You of the non-compliance by
     some reasonable means prior to 60 days after You have come back into
     compliance. Moreover, Your grants from a particular Contributor are
     reinstated on an ongoing basis if such Contributor notifies You of the
     non-compliance by some reasonable means, this is the first time You have
     received notice of non-compliance with this License from such
     Contributor, and You become compliant prior to 30 days after Your receipt
     of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
     infringement claim (excluding declaratory judgment actions,
     counter-claims, and cross-claims) alleging that a Contributor Version
     directly or indirectly infringes any patent, then the rights granted to
     You by any and all Contributors for the Covered Software under Section
     2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
     license agreements (excluding distributors and resellers) which have been
     validly granted by You or Your distributors under this License prior to
     termination shall survive termination.

6. Disclaimer of Warranty

   Covered Software is provided under this License on an "as is" basis,
   without warranty of any kind, either expressed, implied, or statutory,
   including, without limitation, warranties that the Covered Software is free
   of defects, merchantable, fit for a particular purpose or non-infringing.
   The entire risk as to the quality and performance of the Covered Software
   is with You. Should any Covered Software prove defective in any respect,
   You (not any Contributor) assume the cost of any necessary servicing,
   repair, or correction. This disclaimer of warranty constitutes an essential
   part of this License. No use of  any Covered Software is authorized under
   this License except under this disclaimer.

7. Limitation of Liability

   Under no circumstances and under no legal theory, whether tort (including
   negligence), contract, or otherwise, shall any Contributor, or anyone who
   distributes Covered Software as permitted above, be liable to You for any
   direct, indirect, special, incidental, or consequential damages of any
   character including, without limitation, damages for lost profits, loss of
   goodwill, work stoppage, computer failure or malfunction, or any and all
   other commercial damages or losses, even if such party shall have been
   informed of the possibility of such damages. This limitation of liability
   shall not apply to liability for death or personal injury resulting from
   such party's negligence to the extent applicable law prohibits such
   limitation. Some jurisdictions do not allow the exclusion or limitation of
   incidental or consequential damages, so this exclusion and limitation may
   not apply to You.

8. Litigation

   Any litigation relating to this License may be brought only in the courts
   of a jurisdiction where the defendant maintains its principal place of
   business and such litigation shall be governed by laws of that
   jurisdiction, without reference to its conflict-of-law provisions. Nothing
   in this Section shall prevent a party's ability to bring cross-claims or
   counter-claims.

9. Miscellaneous

   This License represents the complete agreement concerning the subject
   matter hereof. If any provision of this License is held to be
   unenforceable, such provision shall be reformed only to the extent
   necessary to make it enforceable. Any law or regulation which provides that
   the language of a contract shall be construed against the drafter shall not
   be used to construe this License against a Contributor.


10. Versions of the License

10.1. New Versions

      Mozilla Foundation is the license steward. Except as provided in Section
      10.3, no one other than the license steward has the right to modify or
      publish new versions of this License. Each version will be given a
      distinguishing version number.

10.2. Effect of New Versions

      You may distribute the Covered Software under the terms of the version
      of the License under which You originally received the Covered Software,
      or under the terms of any subsequent version published by the license
      steward.

10.3. Modified Versions

      If you create software not governed by this License, and you want to
      create a new license for such software, you may create and use a
      modified version of this License if you rename the license and remove
      any references to the name of the license steward (except to note that
      such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
      Licenses If You choose to distribute Source Code Form that is
      Incompatible With Secondary Licenses under the terms of this version of
      the License, the notice described in Exhibit B of this License must be
      attached.

Exhibit A - Source Code Form License Notice

      This Source Code Form is subject to the
      terms of the Mozilla Public License, v.
      2.0. If a copy of the MPL was not
      distributed with this file, You can
      obtain one at
      http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice

      This Source Code Form is "Incompatible
      With Secondary Licenses", as defined by
      the Mozilla Public License, v. 2.0.



================================================
FILE: README.md
================================================
# [BLEMeshChat Android](https://github.com/OnlyInAmerica/BLEMeshChat) [![Build Status](https://travis-ci.org/OnlyInAmerica/BLEMeshChat.svg?branch=master)](https://travis-ci.org/OnlyInAmerica/BLEMeshChat)

[![Screenshot](http://i.imgur.com/GMtn5ol.png)](http://i.imgur.com/GMtn5ol.png)

**Under Development : Not yet ready for use!**

An 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.

Also see the [iOS client](https://github.com/chrisballinger/BLEMeshChat) and [Protocol Spec](https://github.com/chrisballinger/BLEMeshChat/wiki).

## Motivation

A 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.

Imagine:

Broadcasting the locations of potable water in a disaster scenario without Internet.

Seeking insulin at a crowded festival where cell service is unreliable.

Organizing movements of a large protest where cellular Internet is jammed.

## Goals

+ This system must be able to operate constantly in the background without a significant effect on battery life.
+ Connections to peers must be made without user intervention.
+ Messages must be signed, and the system must allow the user to verify other users' association with a particular public key.

## License

MPL 2.0

================================================
FILE: android-wait-for-emulator.sh
================================================
#!/bin/bash

# Originally written by Ralf Kistner <ralf@embarkmobile.com>, but placed in the public domain

set +e

bootanim=""
failcounter=0
timeout_in_sec=360

until [[ "$bootanim" =~ "stopped" ]]; do
  bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &`
  if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
    || "$bootanim" =~ "running" ]]; then
    let "failcounter += 1"
    echo "Waiting for emulator to start"
    if [[ $failcounter -gt timeout_in_sec ]]; then
      echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
      exit 1
    fi
  fi
  sleep 1
done

echo "Emulator is ready"


================================================
FILE: app/.gitignore
================================================
/build


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
apply plugin: 'com.jakewharton.hugo'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "pro.dbro.ble"
        minSdkVersion 21
        targetSdkVersion 22

        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.1.1'
    compile project(':submodules:airshare:sdk')
    apt 'net.simonvt.schematic:schematic-compiler:0.5.1'
    compile 'com.jakewharton.timber:timber:2.7.1'
    compile 'com.google.guava:guava:18.0'
    compile 'net.simonvt.schematic:schematic:0.5.3'
    compile 'com.android.support:support-annotations:20.0.0'
    compile 'com.jakewharton:butterknife:5.1.2'
    compile 'com.android.support:recyclerview-v7:21.0.3'
    compile 'com.android.support:cardview-v7:21.0.0'
    compile 'com.android.support:palette-v7:21.0.0'
    compile 'com.nispok:snackbar:2.10.6'
    compile 'com.facebook.stetho:stetho:1.1.1'
}

apt {
    arguments {
        schematicOutPackage 'pro.dbro.ble.schematic'
    }
}



================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}


================================================
FILE: app/src/androidTest/java/pro/dbro/ble/ChatAppTest.java
================================================
package pro.dbro.ble;

import android.app.Application;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.test.ApplicationTestCase;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;

import pro.dbro.ble.crypto.KeyPair;
import pro.dbro.ble.crypto.SodiumShaker;
import pro.dbro.ble.data.ContentProviderStore;
import pro.dbro.ble.data.model.ChatContentProvider;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.data.model.PeerTable;
import pro.dbro.ble.protocol.BLEProtocol;
import pro.dbro.ble.protocol.IdentityPacket;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.util.RandomString;

/**
 * Tests of the ChatProtocol and Chat Application.
 */
public class ChatAppTest extends ApplicationTestCase<Application> {
    public ChatAppTest() {
        super(Application.class);
    }

    ChatClient mApp;
    OwnedIdentityPacket mSenderIdentity;
    boolean mCreatedNewPrimaryIdentity;
    BLEProtocol bleProtocol = new BLEProtocol();
    ContentProviderStore dataStore;

    protected void setUp() throws Exception {
        super.setUp();

        mApp = new ChatClient(getContext());
        dataStore = new ContentProviderStore(getContext());
        String username = new RandomString(BLEProtocol.ALIAS_LENGTH).nextString();
        KeyPair keyPair =  SodiumShaker.generateKeyPair();
        mSenderIdentity = new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, username, null);
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    /** Protocol Tests **/

    /**
     * {@link pro.dbro.ble.protocol.IdentityPacket} -> byte[] -> {@link pro.dbro.ble.protocol.IdentityPacket}
     */
    public void testCreateAndConsumeIdentityResponse() {
        byte[] identityResponse = bleProtocol.serializeIdentity(mSenderIdentity);

        // Parse Identity from sender's identityResponse response byte[]
        IdentityPacket parsedIdentityPacket = bleProtocol.deserializeIdentity(identityResponse);

        assertEquals(parsedIdentityPacket.alias, mSenderIdentity.alias);
        assertEquals(Arrays.equals(parsedIdentityPacket.publicKey, mSenderIdentity.publicKey), true);
        assertDateIsRecent(parsedIdentityPacket.dateSeen);
    }

    /**
     * {@link pro.dbro.ble.protocol.MessagePacket} -> byte[] -> {@link pro.dbro.ble.protocol.MessagePacket}
     */
    public void testCreateAndConsumeMessageResponse() {
        String messageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString();

        MessagePacket messageResponse = bleProtocol.serializeMessage(mSenderIdentity, messageBody);

        MessagePacket parsedMessagePacket = bleProtocol.deserializeMessage(messageResponse.rawPacket);

        assertEquals(messageBody, parsedMessagePacket.body);
        assertEquals(Arrays.equals(parsedMessagePacket.sender.publicKey, mSenderIdentity.publicKey), true);
        assertDateIsRecent(parsedMessagePacket.authoredDate);
    }

    /** Application Tests **/

    /**
     * Create a {@link pro.dbro.ble.data.model.Peer} for protocol {@link pro.dbro.ble.protocol.IdentityPacket},
     * then create a {@link pro.dbro.ble.data.model.Message} for protocol {@link pro.dbro.ble.protocol.MessagePacket}.
     */
    public void testApplicationIdentityCreationAndMessageConsumption() throws IOException {
        // TODO : Rewrite for new API
        // Get or create new primary identity. This Identity serves as the app user
        Peer user = getOrCreatePrimaryPeerIdentity();

        // User discovers a peer

        IdentityPacket remotePeer = bleProtocol.deserializeIdentity(bleProtocol.serializeIdentity(mSenderIdentity));
        // Assert Identity response parsed successfully
        assertEquals(Arrays.equals(remotePeer.publicKey, mSenderIdentity.publicKey), true);

        // Craft a mock message from remote peer
        String mockReceivedMessageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString();
        MessagePacket mockReceivedMessage = bleProtocol.serializeMessage(mSenderIdentity, mockReceivedMessageBody);

        // User receives mock message from remote peer
//        pro.dbro.ble.data.model.Message parsedMockReceivedMessage = mApp.consumeReceivedBroadcastMessage(getContext(), mockReceivedMessage);
//        assertEquals(mockReceivedMessageBody.equals(parsedMockReceivedMessage.getBody()), true);

        // Cleanup
        // TODO: Should mock database
        int numDeleted = 0;
        if (mCreatedNewPrimaryIdentity) {
            numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS,
                    PeerTable.id + " = ?",
                    new String[]{String.valueOf(user.getId())});
            assertEquals(numDeleted, 1);
            numDeleted = 0;
        }

//        numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS,
//                PeerTable.id + " = ?",
//                new String[] {String.valueOf(remotePeer.getId())});
//
//        assertEquals(numDeleted, 1);
//        numDeleted = 0;
//
//        numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Messages.MESSAGES,
//                MessageTable.id + " = ?",
//                new String[] {String.valueOf(parsedMockReceivedMessage.getId())});
//        assertEquals(numDeleted, 1);
//        numDeleted = 0;
    }

    /**
     * Test database lookups by BLOB column
     */
    public void testDatabaseQueryByBlob() {
        byte[] fakePubKey = new byte[] { (byte) 0x01 };
        ContentValues stubPeer = new ContentValues();
        stubPeer.put(PeerTable.alias, "test");
        stubPeer.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));
        stubPeer.put(PeerTable.pubKey, fakePubKey);
        Uri stubPeerUri = getContext().getContentResolver().insert(ChatContentProvider.Peers.PEERS, stubPeer);

        int stubPeerId = Integer.parseInt(stubPeerUri.getLastPathSegment());

        Cursor result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS,
                null,
                PeerTable.id + " = ?",
                new String[] {
                        String.valueOf(stubPeerId)
                },
                null);

        assertEquals(result != null, true);
        assertEquals(result.moveToFirst(), true);

        byte[] resultBlob = result.getBlob(result.getColumnIndex(PeerTable.pubKey));

        assertEquals(Arrays.equals(resultBlob, fakePubKey), true);
        result.close();

        result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS,
                null,
                "quote(" + PeerTable.pubKey + ") = ?",
                new String[] {
                  "X'01'"
                },
                null);

        assertEquals(result != null, true);
        assertEquals(result.moveToFirst(), true);

        // Cleanup

        int numDeleted = getContext().getContentResolver().delete((ChatContentProvider.Peers.PEERS),
                PeerTable.id + " = ?",
                new String[] {
                        String.valueOf(stubPeerId)
                });
        assertEquals(numDeleted ,1);
    }
    /** Utility **/

    private Peer getOrCreatePrimaryPeerIdentity() throws IOException {
        Peer user = mApp.getPrimaryLocalPeer();
        if (user == null) {
            mCreatedNewPrimaryIdentity = true;
            user =  mApp.createPrimaryIdentity(new RandomString(BLEProtocol.ALIAS_LENGTH).nextString());
        }
        return user;
    }

    private void assertDateIsRecent(Date mustBeRecent) {
        long now = new Date().getTime();
        long oneSecondAgo = now - 1000;

        if ( (mustBeRecent.getTime() > now) ){
            throw new IllegalStateException("Parsed Identity time is from the future " + mustBeRecent);

        } else if (mustBeRecent.getTime() < oneSecondAgo) {
            throw new IllegalStateException("Parsed Identity time is from more than 500ms ago " + mustBeRecent);
        }
    }
}

================================================
FILE: app/src/androidTest/java/pro/dbro/ble/util/RandomString.java
================================================
package pro.dbro.ble.util;

import java.util.Random;

public class RandomString {

    private static final char[] symbols;

    static {
        StringBuilder tmp = new StringBuilder();
        for (char ch = '0'; ch <= '9'; ++ch)
            tmp.append(ch);
        for (char ch = 'a'; ch <= 'z'; ++ch)
            tmp.append(ch);
        symbols = tmp.toString().toCharArray();
    }

    private final Random random = new Random();

    private final char[] buf;

    public RandomString(int length) {
        if (length < 1)
            throw new IllegalArgumentException("length < 1: " + length);
        buf = new char[length];
    }

    public String nextString() {
        for (int idx = 0; idx < buf.length; ++idx)
            buf[idx] = symbols[random.nextInt(symbols.length)];
        return new String(buf);
    }
}

================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="pro.dbro.ble" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name=".ChatApp">

        <provider
            android:name=".schematic.ChatContentProvider"
            android:authorities="pro.dbro.ble.chatprovider"
            android:exported="true">
        </provider>

        <service android:name="pro.dbro.airshare.app.AirShareService" />

        <activity
            android:name=".ui.activities.MainActivity"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


================================================
FILE: app/src/main/java/com/google/samples/apps/iosched/ui/widget/ScrimInsetsScrollView.java
================================================
/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.samples.apps.iosched.ui.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.widget.ScrollView;

import pro.dbro.ble.R;

/**
 * A layout that draws something in the insets passed to {@link #fitSystemWindows(Rect)}, i.e. the area above UI chrome
 * (status and navigation bars, overlay action bars).
 */
public class ScrimInsetsScrollView extends ScrollView {
    private Drawable mInsetForeground;

    private Rect mInsets;
    private Rect mTempRect = new Rect();
    private OnInsetsCallback mOnInsetsCallback;

    public ScrimInsetsScrollView(Context context) {
        super(context);
        init(context, null, 0);
    }

    public ScrimInsetsScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public ScrimInsetsScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs, defStyle);
    }

    private void init(Context context, AttributeSet attrs, int defStyle) {
        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ScrimInsetsView, defStyle, 0);
        if (a == null) {
            return;
        }
        mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsView_insetForeground);
        a.recycle();

        setWillNotDraw(true);
    }

    @Override
    protected boolean fitSystemWindows(Rect insets) {
        mInsets = new Rect(insets);
        setWillNotDraw(mInsetForeground == null);
        ViewCompat.postInvalidateOnAnimation(this);
        if (mOnInsetsCallback != null) {
            mOnInsetsCallback.onInsetsChanged(insets);
        }
        return true; // consume insets
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        int width = getWidth();
        int height = getHeight();
        if (mInsets != null && mInsetForeground != null) {
            int sc = canvas.save();
            canvas.translate(getScrollX(), getScrollY());

            // Top
            mTempRect.set(0, 0, width, mInsets.top);
            mInsetForeground.setBounds(mTempRect);
            mInsetForeground.draw(canvas);

            // Bottom
            mTempRect.set(0, height - mInsets.bottom, width, height);
            mInsetForeground.setBounds(mTempRect);
            mInsetForeground.draw(canvas);

            // Left
            mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom);
            mInsetForeground.setBounds(mTempRect);
            mInsetForeground.draw(canvas);

            // Right
            mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom);
            mInsetForeground.setBounds(mTempRect);
            mInsetForeground.draw(canvas);

            canvas.restoreToCount(sc);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mInsetForeground != null) {
            mInsetForeground.setCallback(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mInsetForeground != null) {
            mInsetForeground.setCallback(null);
        }
    }

    /**
     * Allows the calling container to specify a callback for custom processing when insets change (i.e. when
     * {@link #fitSystemWindows(Rect)} is called. This is useful for setting padding on UI elements based on
     * UI chrome insets (e.g. a Google Map or a ListView). When using with ListView or GridView, remember to set
     * clipToPadding to false.
     */
    public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) {
        mOnInsetsCallback = onInsetsCallback;
    }

    public static interface OnInsetsCallback {
        public void onInsetsChanged(Rect insets);
    }
}

================================================
FILE: app/src/main/java/im/delight/android/identicons/AsymmetricIdenticon.java
================================================
package im.delight.android.identicons;

/**
 * Copyright 2014 www.delight.im <info@delight.im>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;

public class AsymmetricIdenticon extends Identicon {

	public AsymmetricIdenticon(Context context) {
		super(context);
	}

	public AsymmetricIdenticon(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public AsymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}

	@Override
	protected boolean isCellVisible(int row, int column) {
		return getByte(3 + row * getColumnCount() + column) >= 0;
	}

	@Override
	protected int getIconColor() {
		return Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128);
	}

	@Override
	protected int getRowCount() {
		return 4;
	}

	@Override
	protected int getColumnCount() {
		return 4;
	}

}


================================================
FILE: app/src/main/java/im/delight/android/identicons/Identicon.java
================================================
package im.delight.android.identicons;

/**
 * Copyright 2014 www.delight.im <info@delight.im>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import java.security.MessageDigest;

abstract public class Identicon extends View {
	
	private static final String HASH_ALGORITHM = "SHA-256";
	private final int mRowCount;
	private final int mColumnCount;
	private final Paint mPaint;
	private volatile int mCellWidth;
	private volatile int mCellHeight;
	private volatile byte[] mHash;
	private volatile int[][] mColors;
	private volatile boolean mReady;

	public Identicon(Context context) {
		super(context);

		mRowCount = getRowCount();
		mColumnCount = getColumnCount();
		mPaint = new Paint();

		init();
	}

	public Identicon(Context context, AttributeSet attrs) {
		super(context, attrs);

		mRowCount = getRowCount();
		mColumnCount = getColumnCount();
		mPaint = new Paint();

		init();
	}

	public Identicon(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);

		mRowCount = getRowCount();
		mColumnCount = getColumnCount();
		mPaint = new Paint();

		init();
	}
		
	@SuppressLint("NewApi")
	protected void init() {
		mPaint.setStyle(Paint.Style.FILL);
		mPaint.setAntiAlias(true);
		mPaint.setDither(true);
		
		setWillNotDraw(false);
		if (Build.VERSION.SDK_INT >= 11) {
			setLayerType(View.LAYER_TYPE_SOFTWARE, null);
		}
	}

	public void show(String input) {
		// if the input was null
		if (input == null) {
			// we can't create a hash value and have nothing to show (draw to the view)
			mHash = null;
		}
		// if the input was a proper string (non-null)
		else {
			// generate a hash from the string to get unique but deterministic byte values 
			try {
				final MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
				digest.update(input == null ? new byte[0] : input.getBytes());
				mHash = digest.digest();
			}
			catch (Exception e) {
				mHash = null;
			}
		}
		
		// set up the cell colors according to the input that was provided via show(...)
		setupColors();
		
		// this view may now be drawn (and thus must be re-drawn)
		mReady = true;
		invalidate();
	}
	
	public void show(int input) {
		show(String.valueOf(input));
	}
	
	public void show(long input) {
		show(String.valueOf(input));
	}
	
	public void show(float input) {
		show(String.valueOf(input));
	}
	
	public void show(double input) {
		show(String.valueOf(input));
	}
	
	public void show(byte input) {
		show(String.valueOf(input));
	}
	
	public void show(char input) {
		show(String.valueOf(input));
	}
	
	public void show(boolean input) {
		show(String.valueOf(input));
	}
	
	public void show(Object input) {
		if (input == null) {
			mHash = null;
		}
		else {
			show(String.valueOf(input));
		}
	}
	
	protected void setupColors() {
		mColors = new int[mRowCount][mColumnCount];
		int colorVisible = getIconColor();

		for (int r = 0; r < mRowCount; r++) {
			for (int c = 0; c < mColumnCount; c++) {
				if (isCellVisible(r, c)) {
					mColors[r][c] = colorVisible;
				}
				else {
					mColors[r][c] = Color.TRANSPARENT;
				}
			}
		}
	}
	
	protected byte getByte(int index) {
		if (mHash == null) {
			return -128;
		}
		else {
			return mHash[index % mHash.length];
		}
	}
	
	abstract protected int getRowCount();
	
	abstract protected int getColumnCount();

	abstract protected boolean isCellVisible(int row, int column);

	abstract protected int getIconColor();
	
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		
		mCellWidth = w / mColumnCount;
		mCellHeight = h / mRowCount;
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int size = Math.min(getMeasuredWidth(), getMeasuredHeight());
		setMeasuredDimension(size, size);
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		if (mReady) {
			int x, y;
			for (int r = 0; r < mRowCount; r++) {
				for (int c = 0; c < mColumnCount; c++) {
					x = mCellWidth * c;
					y = mCellHeight * r;
					
					mPaint.setColor(mColors[r][c]);

					canvas.drawRect(x, y + mCellHeight, x + mCellWidth, y, mPaint);
				}
			}
		}
	}

}


================================================
FILE: app/src/main/java/im/delight/android/identicons/SymmetricIdenticon.java
================================================
package im.delight.android.identicons;

/**
 * Copyright 2014 www.delight.im <info@delight.im>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;

public class SymmetricIdenticon extends Identicon {
	
	private static final int CENTER_COLUMN_INDEX = 3;

	public SymmetricIdenticon(Context context) {
		super(context);
	}

	public SymmetricIdenticon(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public SymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}
	
	protected int getSymmetricColumnIndex(int row) {
		if (row < CENTER_COLUMN_INDEX) {
			return row;
		}
		else {
			return getColumnCount() - row - 1;
		}
	}

	@Override
	protected boolean isCellVisible(int row, int column) {
		return getByte(3 + row * CENTER_COLUMN_INDEX + getSymmetricColumnIndex(column)) >= 0;
	}

	@Override
	protected int getIconColor() {
		return Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128);
	}

	@Override
	protected int getRowCount() {
		return 5;
	}

	@Override
	protected int getColumnCount() {
		return 5;
	}

}


================================================
FILE: app/src/main/java/pro/dbro/ble/ActivityRecevingMessagesIndicator.java
================================================
package pro.dbro.ble;

/**
 * Implemented by a Service or other entity to report an Activity is bound, and thus
 * in the foreground. e.g: Useful to determine whether to post message notifications.
 *
 * Created by davidbrodsky on 11/14/14.
 */
public interface ActivityRecevingMessagesIndicator {

    public boolean isActivityReceivingMessages();

}


================================================
FILE: app/src/main/java/pro/dbro/ble/ChatApp.java
================================================
package pro.dbro.ble;

import android.app.Application;

import com.facebook.stetho.Stetho;

import timber.log.Timber;

/**
 * Created by davidbrodsky on 4/17/15.
 */
public class ChatApp extends Application {

    @Override public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());

            Stetho.initialize(
                    Stetho.newInitializerBuilder(this)
                            .enableDumpapp(
                                    Stetho.defaultDumperPluginsProvider(this))
                            .enableWebKitInspector(
                                    Stetho.defaultInspectorModulesProvider(this))
                            .build());
        }

        // If we abandon Timber logging in this app, enable below line
        // to enable Timber logging in sdk
        //Logging.forceLogging();
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ChatClient.java
================================================
package pro.dbro.ble;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

import java.util.HashMap;

import pro.dbro.airshare.app.AirShareService;
import pro.dbro.airshare.transport.Transport;
import pro.dbro.ble.data.ContentProviderStore;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.protocol.BLEProtocol;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.protocol.Protocol;
import pro.dbro.ble.ui.Notification;
import pro.dbro.ble.ui.activities.LogConsumer;
import timber.log.Timber;

/**
 * Created by davidbrodsky on 10/13/14.
 */
public class ChatClient implements AirShareService.Callback,
                                   ChatPeerFlow.DataOutlet,
                                   ChatPeerFlow.Callback {

    public interface Callback {
        /** Client should not invoke remotePeer#close() */
        void onAppPeerStatusUpdated(@NonNull Peer remotePeer,
                                    @NonNull ConnectionStatus status);
    }

    public static final String TAG = "ChatApp";
    public static final String AIRSHARE_SERVICE_NAME = "BLEMeshChat";

    private Context   mContext;
    private DataStore mDataStore;
    private Protocol  mProtocol;
    private AirShareService.ServiceBinder mAirShareServiceBinder;
    private Callback mCallback;

    private HashMap<pro.dbro.airshare.session.Peer, ChatPeerFlow> mFlows = new HashMap<>();

    /** AirShare Peer -> BLEMeshChat Peer id */
    private BiMap<pro.dbro.airshare.session.Peer, Integer> mConnectedPeers = HashBiMap.create();

    // <editor-fold desc="Public API">

    public ChatClient(@NonNull Context context) {
        mContext = context;

        mProtocol  = new BLEProtocol();
        mDataStore = new ContentProviderStore(context);
    }

    public void setAirShareServiceBinder(AirShareService.ServiceBinder binder) {
        mAirShareServiceBinder = binder;
        mAirShareServiceBinder.setCallback(this);
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    // <editor-fold desc="Identity & Availability">

    public void makeAvailable() {
        if (mDataStore.getPrimaryLocalPeer() == null) {
            Timber.e("No primary Identity. Cannot make client available");
            return;
        }

        if (mAirShareServiceBinder == null) {
            Timber.e("No AirShareBinder set. Cannot make available");
            return;
        }

        mAirShareServiceBinder.advertiseLocalUser();
        mAirShareServiceBinder.scanForOtherUsers();
    }

    public void makeUnavailable() {
        if (mAirShareServiceBinder == null) {
            Timber.e("No AirShareBinder set. Cannot make unavailable");
            return;
        }

        mAirShareServiceBinder.stop();
    }

    public Peer getPrimaryLocalPeer() {
        return mDataStore.getPrimaryLocalPeer();
    }

    public Peer createPrimaryIdentity(String alias) {
        // TODO Test if this should be moved to background thread and async call?
        return mDataStore.createLocalPeerWithAlias(alias, mProtocol);
    }

    // </editor-fold desc="Identity & Availability">

    // <editor-fold desc="Messages">

    public void sendPublicMessageFromPrimaryIdentity(String body) {
        MessagePacket messagePacket = mProtocol.serializeMessage((OwnedIdentityPacket) getPrimaryLocalPeer().getIdentity(), body);
        mDataStore.createOrUpdateMessageWithProtocolMessage(messagePacket).close();
        // TODO : Send to connected peers. Future peers will get message during flow
        if (mAirShareServiceBinder != null) {

            for (pro.dbro.airshare.session.Peer peer : mConnectedPeers.keySet()) {
                ChatPeerFlow flow = mFlows.get(peer);
                // If we're actively flowing with a peer, add the message to that flow
                // else, send immediately
                if (flow != null && !flow.isComplete())
                    flow.queueMessage(messagePacket);
                else
                    mAirShareServiceBinder.send(messagePacket.rawPacket, peer);
            }

        }
    }

    // </editor-fold desc="Messages">

    public DataStore getDataStore() {
        return mDataStore;
    }

    // </editor-fold desc="Public API">

    // <editor-fold desc="Private API">

    @Override
    public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,
                                       @NonNull Peer remotePeer,
                                       @NonNull ConnectionStatus status) {

        Timber.d("%s %s", remotePeer.getAlias(), status == ConnectionStatus.CONNECTED ? "connected" : "disconnected");
        if (mCallback != null)
            mCallback.onAppPeerStatusUpdated(remotePeer, status);

        if (!mAirShareServiceBinder.isActivityReceivingMessages())
            Notification.displayPeerAvailableNotification(mContext, remotePeer, status == ConnectionStatus.CONNECTED);

        switch (status) {
            case CONNECTED:
                mConnectedPeers.put(flow.getRemoteAirSharePeer(), remotePeer.getId());
                break;

            case DISCONNECTED:
                mConnectedPeers.remove(flow.getRemoteAirSharePeer());
                break;
        }
    }

    @Override
    public void onMessageSent(@NonNull ChatPeerFlow flow, @NonNull Message message, @NonNull Peer recipient) {
        Timber.d("Sent message: '%s'", message.getBody());
        // TODO : Might be unnecessary
        message.close();
    }

    @Override
    public void onMessageReceived(@NonNull ChatPeerFlow flow, @NonNull Message message, Peer sender) {
        Timber.d("Received message: '%s' with sig '%s' ", message.getBody(), DataUtil.bytesToHex(message.getSignature()).substring(0, 3));

        // We don't check that mAirShareServiceBinder is not null because this callback is provoked
        // by the binder callbacks

        // Send message notification if it's a new message and no Activity is reported active
        if (!mAirShareServiceBinder.isActivityReceivingMessages()) {
            Notification.displayMessageNotification(mContext, message, sender);
            message.close();
        }
    }

    @Override
    public void onDataRecevied(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer sender, @Nullable Exception exception) {
        ChatPeerFlow flow = mFlows.get(sender);

        if (flow == null) {
            Timber.w("No flow for %s", sender.getAlias());
            return;
        }

        try {
            flow.onDataReceived(data);
        } catch (ChatPeerFlow.UnexpectedDataException e) {
            Timber.e(e, "Error processing received data");
        }
    }

    @Override
    public void onDataSent(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer recipient, @Nullable Exception exception) {
        ChatPeerFlow flow = mFlows.get(recipient);

        if (flow == null) {
            Timber.w("No flow for %s", recipient.getAlias());
            return;
        }

        try {
            flow.onDataSent(data);
        } catch (ChatPeerFlow.UnexpectedDataException e) {
            Timber.e(e, "Error processing sent data");
        }
    }

    @Override
    public void onPeerStatusUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, @NonNull Transport.ConnectionStatus newStatus, boolean peerIsHost) {
        if (newStatus == Transport.ConnectionStatus.CONNECTED) {
            mConnectedPeers.put(peer, null); // We will add the BLEMeshChat peer id after identity is received
            Timber.d("Beginning flow with %s as %s", peer.getAlias(), peerIsHost ? "host" : "client");
            mFlows.put(peer, new ChatPeerFlow(mDataStore, mProtocol, this, peer, peerIsHost, this));
        }
        else if (newStatus == Transport.ConnectionStatus.DISCONNECTED) {

            if (!mConnectedPeers.containsKey(peer) || mConnectedPeers.get(peer) == null) {
                if (mConnectedPeers.containsKey(peer)) mConnectedPeers.remove(peer);
                Timber.w("Cannot report peer %s disconnected, no connection record", peer.getAlias());
                return;
            }

            int blePeerId = mConnectedPeers.get(peer);
            Peer remotePeer = mDataStore.getPeerById(blePeerId);
            onAppPeerStatusUpdated(mFlows.get(peer), remotePeer, ConnectionStatus.DISCONNECTED);
        }
    }

    @Override
    public void onPeerTransportUpdated(@NonNull AirShareService.ServiceBinder binder,
                                       @NonNull pro.dbro.airshare.session.Peer peer,
                                       int newTransportCode,
                                       @Nullable Exception exception) {
        // unused. The networking demands of this app appear to works fine over BLE
    }

    @Override
    public void sendData(pro.dbro.airshare.session.Peer peer, byte[] data) {
        if(mAirShareServiceBinder == null) {
            Timber.e("AirShare Service binder is null! Cannot send data");
            return;
        }
        mAirShareServiceBinder.send(data, peer);
    }

    // </editor-fold desc="Private API">
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ChatPeerFlow.java
================================================
package pro.dbro.ble;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

import pro.dbro.airshare.session.Peer;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.MessageCollection;
import pro.dbro.ble.protocol.IdentityPacket;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.NoDataPacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.protocol.Protocol;
import timber.log.Timber;

/**
 * This class orchestrates the flow between two ChatApp Peers, handing network requests and
 * updating the {@link pro.dbro.ble.data.DataStore}. The client of this class may use
 * {@link ChatPeerFlow.Callback} to update their UI or in-memory application state.
 *
 * The general gist of the flow:
 *
 * 1) Client peer writes identity
 * 2) Client peer waits for host identity
 * 3) Client peer writes outgoing messages
 * 4) Client peer waits for incoming messages
 * Created by davidbrodsky on 4/16/15.
 */
public class ChatPeerFlow {

    public static class UnexpectedDataException extends Exception {
        public UnexpectedDataException(String detailMessage) {
            super(detailMessage);
        }
    }

    /** Entity responsible for sending data to a peer */
    public static interface DataOutlet {
        public void sendData(Peer peer, byte[] data);
    }

    public static interface Callback {

        public static enum ConnectionStatus { CONNECTED, DISCONNECTED }

        public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,
                                           @NonNull pro.dbro.ble.data.model.Peer peer,
                                           @NonNull ConnectionStatus status);

        public void onMessageSent(@NonNull ChatPeerFlow flow,
                                  @NonNull Message message,
                                  @NonNull pro.dbro.ble.data.model.Peer recipient);

        public void onMessageReceived(@NonNull ChatPeerFlow flow,
                                      @NonNull Message message,
                                      @Nullable pro.dbro.ble.data.model.Peer sender);

    }

    private static final int MESSAGES_PER_RESPONSE = 50;
    private static final int IDENTITIES_PER_RESPONSE = 10;
    public static enum State { CLIENT_WRITE_ID, HOST_WRITE_ID, CLIENT_WRITE_MSGS, HOST_WRITE_MSGS }

    private State mState = State.CLIENT_WRITE_ID;
    private OwnedIdentityPacket mLocalIdentity;
    private Peer mRemoteAirSharePeer;
    private Protocol mProtocol;
    private DataStore mDataStore;
    private DataOutlet mOutlet;
    private IdentityPacket mRemoteIdentity;
    private Callback mCallback;
    private ArrayDeque<MessagePacket> mMessageOutbox = new ArrayDeque<>();
    private ArrayDeque<IdentityPacket> mIdentityOutbox = new ArrayDeque<>();

    private boolean mPeerIsHost;
    private boolean mIsComplete = false;
    private boolean mFetchedMessages = false;
    private boolean mFetchedIdentities = false;
    private boolean mGotRemotePeerIdentity = false;

    public ChatPeerFlow(DataStore dataStore,
                        Protocol protocol,
                        DataOutlet outlet,
                        Peer remotePeer,
                        boolean peerIsHost,
                        Callback callback) {

        mRemoteAirSharePeer = remotePeer;
        mOutlet = outlet;
        mProtocol = protocol;
        mDataStore = dataStore;
        mLocalIdentity = (OwnedIdentityPacket) dataStore.getPrimaryLocalPeer().getIdentity();
        mPeerIsHost = peerIsHost;
        mCallback = callback;

        // Client initiates flow
        if (mPeerIsHost)
            sendIdentity();
    }

    public boolean isComplete() {
        return mIsComplete;
    }

    public Peer getRemoteAirSharePeer() {
        return mRemoteAirSharePeer;
    }

    public void queueMessage(MessagePacket message) {
        mMessageOutbox.add(message);
    }

    /**
     * Called when data is acknowledged as sent to the peer passed to this instance's constructor
     * @return whether this flow is complete and should not receive further events.
     */
    public boolean onDataSent(byte[] data) throws UnexpectedDataException {
        // When data is ack'd we should be in a local-peer writing state
        if ((!mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete))) ||
            (mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete)))) {

            throw new IllegalStateException(String.format("onDataSent invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host"));

        }

        Timber.d("Sent data %s", DataUtil.bytesToHex(data));

        byte type = mProtocol.getPacketType(data);

        // TODO : Perhaps we should cache last sent item to avoid deserializing bytes we've
        // just serialized in sendData
        switch (mState) {
            case HOST_WRITE_ID:
            case CLIENT_WRITE_ID:

                switch(type) {
                    case IdentityPacket.TYPE:

                        IdentityPacket sentIdPkt = mProtocol.deserializeIdentity(data);
                        mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(sentIdPkt);
                        // We can only report the identity sent once we know the peer's identity
                        // We also always want to send our own identity first
                        if (mRemoteIdentity != null) {
                            Timber.d("Marked identity %s delivered to %s", sentIdPkt.alias, mRemoteIdentity.alias);
                            mDataStore.markIdentityDeliveredToPeer(sentIdPkt, mRemoteIdentity);
                        }

                        mIdentityOutbox.poll();

                        sendAsAppropriate();
                        break;

                    case NoDataPacket.TYPE:

                        incrementStateAndSendAsAppropriate();
                        break;

                    default:
                        throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type));

                }
                break;

            case HOST_WRITE_MSGS:
            case CLIENT_WRITE_MSGS:

                switch(type) {
                    case MessagePacket.TYPE:

                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);
                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);
                        // Mark incoming messages as delivered to sender
                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);
                        mCallback.onMessageSent(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));

                        mMessageOutbox.poll();

                        sendAsAppropriate();
                        break;

                    case NoDataPacket.TYPE:

                        incrementStateAndSendAsAppropriate();
                        break;

                    default:
                        throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type));

                }

                break;

            default:
                Timber.e("Flow received unexpected response from client peer");
        }
        return mIsComplete;
    }

    /**
     * Called when data is received from the peer passed to this instance's constructor
     * @return whether this flow is complete and should not receive further events.
     */
    public boolean onDataReceived(byte[] data) throws UnexpectedDataException {
        // When data comes in we should be in a remote-peer writing state
        if ((!mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete))) ||
            (mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete)))) {

            throw new IllegalStateException(String.format("onDataReceived invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host"));

        }

        //Timber.d("Received data %s", DataUtil.bytesToHex(data));

        byte type = mProtocol.getPacketType(data);

        switch (mState) {
            case HOST_WRITE_ID:
            case CLIENT_WRITE_ID:

                switch(type) {
                    case IdentityPacket.TYPE:

                        mRemoteIdentity = mProtocol.deserializeIdentity(data);
                        Timber.d("Got remote identity for %s", mRemoteIdentity.alias);
                        pro.dbro.ble.data.model.Peer remotePeer = mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(mRemoteIdentity);
                        // Only treat first identity as that of connected peer
                        if (!mGotRemotePeerIdentity) {
                            mCallback.onAppPeerStatusUpdated(this, remotePeer, Callback.ConnectionStatus.CONNECTED);
                            mGotRemotePeerIdentity = true;
                        }
                        break;

                    case NoDataPacket.TYPE:

                        Timber.d("Received identity NoData");
                        incrementStateAndSendAsAppropriate();
                        break;

                    default:

                        throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type));
                }

                break;

            case HOST_WRITE_MSGS:
            case CLIENT_WRITE_MSGS:

                switch (type) {
                    case MessagePacket.TYPE:

                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);
                        Timber.d("Received msg %s", msgPkt.body);

                        // Mark incoming messages as delivered to sender

                        boolean isNewMessage = true;
                        Message existingMessage = mDataStore.getMessageBySignature(msgPkt.signature);
                        if (existingMessage != null) {
                            isNewMessage = false;
                            existingMessage.close();
                        }

                        // TODO : Allow updating a message?
                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);
                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);

                        if (isNewMessage)
                            mCallback.onMessageReceived(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));

                        break;

                    case NoDataPacket.TYPE:

                        Timber.d("Received msg NoData");
                        incrementStateAndSendAsAppropriate();
                        break;

                    default:

                        throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type));

                }
                break;

            default:
                Timber.e("Flow received unexpected response from client peer");
        }
        return mIsComplete;
    }

    private void sendIdentity() {
        if (!mFetchedIdentities) {

            // If we're the client, we're initiating the identity flow, and we won't have the remote identity yet
            mIdentityOutbox.addAll(getIdentitiesForIdentity(mRemoteIdentity == null ? null : mRemoteIdentity.publicKey,
                    IDENTITIES_PER_RESPONSE));
            mFetchedIdentities = true;
        }

        Timber.d("Send identity %s", mIdentityOutbox.size() == 0 ? "NoData" : "");
        mOutlet.sendData(mRemoteAirSharePeer,
                         mIdentityOutbox.size() == 0 ?
                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :
                            mIdentityOutbox.peek().rawPacket);
    }

    private void sendMessage() {
        if (!mFetchedMessages) {
            mMessageOutbox.addAll(getMessagesForIdentity(mRemoteIdentity.publicKey, MESSAGES_PER_RESPONSE));
            mFetchedMessages = true;
        }

        Timber.d("Send message %s", mMessageOutbox.size() == 0 ? "NoData" : "");
        mOutlet.sendData(mRemoteAirSharePeer,
                         mMessageOutbox.size() == 0 ?
                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :
                            mMessageOutbox.peek().rawPacket);
    }

    private void incrementStateAndSendAsAppropriate() {
        if (mState == State.HOST_WRITE_MSGS) {
            Timber.d("ChatPeerFlow complete!");
            mIsComplete = true;
            return;
        }

        mState = State.values()[mState.ordinal() + 1];
        Timber.d("ChatPeerFlow New State : %s", mState);
        sendAsAppropriate();
    }

    private void sendAsAppropriate() {

        switch (mState) {
            case CLIENT_WRITE_ID:
                if (mPeerIsHost) sendIdentity();
                break;

            case HOST_WRITE_ID:
                if (!mPeerIsHost) sendIdentity();
                break;

            case CLIENT_WRITE_MSGS:
                if (mPeerIsHost) sendMessage();
                break;

            case HOST_WRITE_MSGS:
                if (!mPeerIsHost) sendMessage();
                break;
        }
    }

    /**
     * Return a queue of message packets for delivery to remote identity with given public key.
     *
     * If recipientPublicKey is null, queues most recent messages
     */
    private ArrayDeque<MessagePacket> getMessagesForIdentity(@Nullable byte[] recipientPublicKey, int maxMessages) {
        ArrayDeque<MessagePacket> messagePacketQueue = new ArrayDeque<>();

        if (recipientPublicKey != null) {
            // Get messages not delievered to peer
            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);
            List<MessagePacket> messages = mDataStore.getOutgoingMessagesForPeer(recipient, maxMessages);

            if (messages == null || messages.size() == 0) {
                Timber.d("Got no messages for peer with pub key " + DataUtil.bytesToHex(recipientPublicKey));
            } else {
                messagePacketQueue.addAll(messages);
            }
        } else {
            // Get most recent messages
            MessageCollection recentMessages = mDataStore.getRecentMessages();
            for (int x = 0; x < Math.min(maxMessages, recentMessages.getCursor().getCount()); x++) {
                Message currentMessage = recentMessages.getMessageAtPosition(x);
                if (currentMessage != null)
                    messagePacketQueue.add(currentMessage.getProtocolMessage(mDataStore));
            }
            recentMessages.close();
        }
        return messagePacketQueue;
    }

    /**
     * Return a queue of identity packets for delivery to the remote identity with the given
     * public key.
     *
     * If recipientPublicKey is null, or no messages undelivered for recipient,
     * the user identity will be queued. As such this method will never return a null
     * or empty queue. Thus it should only be called once per flow and should not
     * be used as an indication of whether identity transmission with a peer is complete.
     */
    private ArrayDeque<IdentityPacket> getIdentitiesForIdentity(@Nullable byte[] recipientPublicKey, int maxIdentities) {
        List<IdentityPacket> identities = null;
        ArrayDeque<IdentityPacket> identityPacketQueue = new ArrayDeque<>();
        if (recipientPublicKey != null) {
            // We have a public key for the remote peer, fetch undelivered identities
            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);
            identities = mDataStore.getOutgoingIdentitiesForPeer(recipient, maxIdentities);
        }

        if (identities == null || identities.size() == 0) {
            Timber.d("Got no identities to send for peer %s. Sending own identity", recipientPublicKey == null ? "" : "with pub key " + DataUtil.bytesToHex(recipientPublicKey).substring(2, 6));
            // For now, at least send our identity
            if (identities == null) identities = new ArrayList<>(1);
            identities.add(mDataStore.getPrimaryLocalPeer().getIdentity());
        }
        identityPacketQueue.addAll(identities);

        return identityPacketQueue;
    }

}


================================================
FILE: app/src/main/java/pro/dbro/ble/PrefsManager.java
================================================
package pro.dbro.ble;

import android.content.Context;


/**
 * Created by davidbrodsky on 9/21/14.
 */
public class PrefsManager {

    /** SharedPreferences store names */
    private static final String APP_PREFS = "prefs";

    /** SharedPreferences keys */
    private static final String APP_STATUS = "status";

    public static int getStatus(Context context) {
        return context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE)
                      .getInt(APP_STATUS, 0);
    }

    public static void setStatus(Context context, int status) {
        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit()
               .putInt(APP_STATUS, status)
               .commit();
    }

    public static void clearState(Context context) {
        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit().clear().apply();
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/crypto/KeyPair.java
================================================
package pro.dbro.ble.crypto;

/**
 * Created by davidbrodsky on 10/22/14.
 */
public class KeyPair {

    public final byte[] publicKey;
    public final byte[] secretKey;

    public KeyPair(byte[] publicKey, byte[] secretKey) {
        this.publicKey = publicKey;
        this.secretKey = secretKey;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/crypto/SodiumShaker.java
================================================
package pro.dbro.ble.crypto;

import android.support.annotation.NonNull;

import org.abstractj.kalium.NaCl;
import org.abstractj.kalium.Sodium;

/**
 * Wrapper around libsodium functions.
 *
 * Created by davidbrodsky on 10/13/14.
 */
public class SodiumShaker {
    private static final String TAG = "Identity";

    public static final int crypto_sign_PUBLICKEYBYTES = 32;
    private static final int crypto_sign_SECRETKEYBYTES = 64;
    public static final int crypto_sign_BYTES = 64;

    static {
        // Load native libraries
        NaCl.sodium();
        // Initialize libsodium
        if (Sodium.sodium_init() == -1) {
            throw new IllegalStateException("sodiun_init failed!");
        }
    }

    public static KeyPair generateKeyPair() {
        byte[] pk = new byte[crypto_sign_PUBLICKEYBYTES];
        byte[] sk = new byte[crypto_sign_SECRETKEYBYTES];

        Sodium.crypto_sign_ed25519_keypair(pk, sk);
        return new KeyPair(pk, sk);
    }

    public static byte[] generateSignatureForMessage(@NonNull byte[] secret_key, @NonNull byte[] message, int message_len) {
        if (secret_key.length != crypto_sign_SECRETKEYBYTES) throw new IllegalArgumentException("secret_key is incorrect length");
        byte[] signature = new byte[crypto_sign_BYTES];
        int[] signature_len = new int[0];

        Sodium.crypto_sign_ed25519_detached(signature, signature_len, message, message_len, secret_key);

        return signature;
    }

    /**
     * Very that signature and public_key verify message
     *
     * @param public_key the public key corresponding to signature
     * @param signature the signature of message decipherable with public_key
     * @param message the data with signature
     */
    public static boolean verifySignature(@NonNull byte[] public_key, @NonNull byte[] signature, @NonNull byte[] message) {
        // Verify signature

        if (Sodium.crypto_sign_ed25519_verify_detached(signature, message, message.length, public_key) != 0) {
            /* Incorrect signature! */
            return false;
        }
        return true;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/ContentProviderStore.java
================================================
package pro.dbro.ble.data;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import pro.dbro.ble.crypto.KeyPair;
import pro.dbro.ble.crypto.SodiumShaker;
import pro.dbro.ble.data.model.ChatContentProvider;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.IdentityDeliveryTable;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.MessageCollection;
import pro.dbro.ble.data.model.MessageDeliveryTable;
import pro.dbro.ble.data.model.MessageTable;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.data.model.PeerTable;
import pro.dbro.ble.protocol.IdentityPacket;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.protocol.Protocol;

/**
 * API for the application's data persistence
 *
 * If the underlying data storage were to be replaced, this should be the
 * only class requiring modification.
 *
 * Created by davidbrodsky on 10/20/14.
 */
public class ContentProviderStore extends DataStore {
    public static final String TAG = "DataManager";

    public ContentProviderStore(Context context) {
        super(context);
    }

    @Override
    public void markMessageDeliveredToPeer(@NonNull MessagePacket messagePacket, @NonNull IdentityPacket recipientPacket) {
        Message message = getMessageBySignature(messagePacket.signature);
        Peer recipient = getPeerByPubKey(recipientPacket.publicKey);

        if (message == null || recipient == null) {
            Log.w(TAG, "Unable to record message delivery. No peer or message database id available");
            return;
        }

        ContentValues delivery = new ContentValues();
        delivery.put(MessageDeliveryTable.messageId, message.getId());
        delivery.put(MessageDeliveryTable.peerId, recipient.getId());

        mContext.getContentResolver().insert(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, delivery);
        Log.i(TAG, "Recorded message delivery");
        message.close();
    }

    @Override
    public void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity) {
        Peer payloadPeer = getPeerByPubKey(payloadIdentity.publicKey);
        Peer recipientPeer = getPeerByPubKey(recipientIdentity.publicKey);

        if (payloadPeer == null || recipientPeer == null) {
            Log.w(TAG, "Unable to fetch payload or recipient identity. Cannot mark identity delivered");
            return;
        }

        ContentValues delivery = new ContentValues();
        delivery.put(IdentityDeliveryTable.peerPayloadId, payloadPeer.getId());
        delivery.put(IdentityDeliveryTable.peerRecipientId, recipientPeer.getId());

        mContext.getContentResolver().insert(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES, delivery);
        Log.i(TAG, "Recorded identity delivery");
    }

    @Nullable
    @Override
    public Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol) {
        KeyPair keyPair = SodiumShaker.generateKeyPair();
        ContentValues dbEntry = new ContentValues();
        dbEntry.put(PeerTable.pubKey, keyPair.publicKey);
        dbEntry.put(PeerTable.secKey, keyPair.secretKey);
        dbEntry.put(PeerTable.alias, alias);
        dbEntry.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));
        if (protocol != null) {
            // If protocol is available, use it to cache the Identity packet for transmission
            dbEntry.put(PeerTable.rawPkt, protocol.serializeIdentity(
                    new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, alias, null)));
        }
        Uri newIdentityUri = mContext.getContentResolver().insert(ChatContentProvider.Peers.PEERS, dbEntry);
        return getPeerById(Integer.parseInt(newIdentityUri.getLastPathSegment()));
    }

    /**
     * @return the first user peer entry in the database,
     * or null if no identity is set.
     */
    @Override
    @Nullable
    public Peer getPrimaryLocalPeer() {
        // TODO: caching
        Cursor result = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS,
                null,
                PeerTable.secKey + " IS NOT NULL",
                null,
                null);
        if (result != null && result.moveToFirst()) {
            Peer peer = new Peer(result);
            result.close();
            return peer;
        }
        return null;
    }

    @Nullable
    @Override
    public List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages) {
        // TODO : Don't send messages past a certain age etc?
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null, null, null, null);
        if (messagesCursor != null) {
            List<MessagePacket> messagesToSend = new ArrayList<>();
            while (messagesCursor.moveToNext()) {
                Message individualMessage = new Message(messagesCursor);
                if (!haveDeliveredMessageToPeer(individualMessage, recipient)) {
                    messagesToSend.add(individualMessage.getProtocolMessage(this));
                    if (messagesToSend.size() > maxMessages) break;
                }
            }

            messagesCursor.close();
            return messagesToSend;
        }
        return null;
    }

    @Override
    public List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxIdentities) {
        // TODO : Don't send identities past a certain age etc?
        Cursor identitiesCursor = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS, null, null, null, null);
        if (identitiesCursor != null) {
            List<IdentityPacket> identitiesToSend = new ArrayList<>();
            while (identitiesCursor.moveToNext()) {
                Peer payloadPeer = new Peer(identitiesCursor);
                if (!haveDeliveredPeerIdentityToPeer(payloadPeer, recipient)) {
                    identitiesToSend.add(payloadPeer.getIdentity());
                    if (identitiesToSend.size() > maxIdentities) break;
                }

            }

            identitiesCursor.close();
            return identitiesToSend;
        }
        return null;
    }

    @Override
    public MessageCollection getRecentMessages() {
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,
                null,
                null,
                null,
                MessageTable.receivedDate + " DESC");

        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {
            return new MessageCollection(messagesCursor);
        }
        return null;
    }

    @Override
    public MessageCollection getRecentMessagesByPeer(@NonNull Peer author) {
        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,
                null,
                MessageTable.peerId + "=?",
                new String[] { String.valueOf(author.getId()) },
                MessageTable.receivedDate + " DESC");

        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {
            return new MessageCollection(messagesCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket remoteIdentityPacket) {
        // Query if peer exists
        Peer peer = getPeerByPubKey(remoteIdentityPacket.publicKey);

        ContentValues peerValues = new ContentValues();
        peerValues.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));
        peerValues.put(PeerTable.pubKey, remoteIdentityPacket.publicKey);
        peerValues.put(PeerTable.alias, remoteIdentityPacket.alias);
        peerValues.put(PeerTable.rawPkt, remoteIdentityPacket.rawPacket);

        if (peer != null) {
            // Peer exists. Modify lastSeenDate
            Log.i(TAG, "Updating peer for pubkey " + DataUtil.bytesToHex(remoteIdentityPacket.publicKey));

            int updated = mContext.getContentResolver().update(
                    ChatContentProvider.Peers.PEERS,
                    peerValues,
                    "quote("+ PeerTable.pubKey + ") = ?" ,
                    new String[] {DataUtil.bytesToHex(remoteIdentityPacket.publicKey)});
            if (updated != 1) {
                Log.e(TAG, "Failed to update peer last seen");
            }
        } else {
            // Peer does not exist. Create.
            Uri peerUri = mContext.getContentResolver().insert(
                    ChatContentProvider.Peers.PEERS,
                    peerValues);

            // Fetch newly created peer
            peer = getPeerById(Integer.parseInt(peerUri.getLastPathSegment()));
            Log.i(TAG, String.format("Created new peer %d for pubkey %s", Integer.parseInt(peerUri.getLastPathSegment()), DataUtil.bytesToHex(remoteIdentityPacket.publicKey)));

            if (peer == null) {
                Log.e(TAG, "Failed to query peer after insertion.");
            }
        }
        return peer;
    }

    @Nullable
    @Override
    public Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket) {
        // Query if peer exists
        Peer peer = getPeerByPubKey(protocolMessagePacket.sender.publicKey);

        if (peer == null)
            throw new IllegalStateException("Failed to get peer for message");

        // See if message exists
        Message message = getMessageBySignature(protocolMessagePacket.signature);
        if (message == null) {
            // Message doesn't exist in our database

            // Insert message into database
            ContentValues newMessageEntry = new ContentValues();
            newMessageEntry.put(MessageTable.body, protocolMessagePacket.body);
            newMessageEntry.put(MessageTable.peerId, peer.getId());
            newMessageEntry.put(MessageTable.receivedDate, DataUtil.storedDateFormatter.format(new Date()));
            newMessageEntry.put(MessageTable.authoredDate, DataUtil.storedDateFormatter.format(protocolMessagePacket.authoredDate));
            newMessageEntry.put(MessageTable.signature, protocolMessagePacket.signature);
            newMessageEntry.put(MessageTable.replySig, protocolMessagePacket.replySig);
            newMessageEntry.put(MessageTable.rawPacket, protocolMessagePacket.rawPacket);

            Uri newMessageUri = mContext.getContentResolver().insert(
                    ChatContentProvider.Messages.MESSAGES,
                    newMessageEntry);
            message = getMessageById(Integer.parseInt(newMessageUri.getLastPathSegment()));
        } else {
            // We already have a message with this signature
            // Since we currently don't have any mutable message fields (e.g hopcount)
            // do nothing
            Log.i(TAG, "Received stored message. Ignoring");
        }

        return message;
    }

    @Nullable
    @Override
    public Message getMessageBySignature(@NonNull byte[] signature) {
        Cursor messageCursor = mContext.getContentResolver().query(
                ChatContentProvider.Messages.MESSAGES,
                null,
                "quote(" + MessageTable.signature + ") = ?",
                new String[] {DataUtil.bytesToHex(signature)},
                null);
        if (messageCursor != null && messageCursor.moveToFirst()) {
            return new Message(messageCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Message getMessageById(int id) {
        Cursor messageCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null,
                MessageTable.id + " = ?",
                new String[]{String.valueOf(id)},
                null);
        if (messageCursor != null && messageCursor.moveToFirst()) {
            return new Message(messageCursor);
        }
        return null;
    }

    @Nullable
    @Override
    public Peer getPeerByPubKey(@NonNull byte[] publicKey) {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                null,
                "quote(" + PeerTable.pubKey + ") = ?",
                new String[] {DataUtil.bytesToHex(publicKey)},
                null);
        if (peerCursor != null && peerCursor.moveToFirst()) {
            Peer peer = new Peer(peerCursor);
            peerCursor.close();
            return peer;
        }
        return null;
    }

    @Nullable
    @Override
    public Peer getPeerById(int id) {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                null,
                PeerTable.id + " = ?",
                new String[] {String.valueOf(id)},
                null);
        if (peerCursor != null && peerCursor.moveToFirst()) {
            Peer peer = new Peer(peerCursor);
            peerCursor.close();
            return peer;
        }
        return null;
    }

    @Override
    public int countPeers() {
        Cursor peerCursor = mContext.getContentResolver().query(
                ChatContentProvider.Peers.PEERS,
                new String[] {PeerTable.id},
                null,
                null,
                null);
        if (peerCursor != null) {
            int result = peerCursor.getCount();
            peerCursor.close();
            return result;
        }
        return 0;
    }

    @Override
    public int countMessagesPassed() {
        Cursor deliveryCursor = mContext.getContentResolver().query(
                ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,
                new String[] {MessageDeliveryTable.id},
                null,
                null,
                null);
        if (deliveryCursor != null) {
            int result = deliveryCursor.getCount();
            deliveryCursor.close();
            return result;
        }
        return 0;
    }

    /** Utility */

    private boolean haveDeliveredMessageToPeer(Message message, Peer peer) {
        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,
                null,
                MessageDeliveryTable.messageId + " = ? AND " + MessageDeliveryTable.peerId + " = ?",
                new String[]{String.valueOf(message.getId()), String.valueOf(peer.getId())},
                null);
        try {
            return deliveryCursor != null && deliveryCursor.moveToFirst();
        } finally {
            if (deliveryCursor != null) deliveryCursor.close();
        }
    }

    /**
     * @return whether peerPayload has been delivered to peerRecipient
     */
    private boolean haveDeliveredPeerIdentityToPeer(Peer peerPayload, Peer peerRecipient) {
        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES,
                null,
                IdentityDeliveryTable.peerRecipientId + " = ? AND " + IdentityDeliveryTable.peerPayloadId + " = ?",
                new String[]{String.valueOf(peerRecipient.getId()), String.valueOf(peerPayload.getId())},
                null);
        try {
            return deliveryCursor != null && deliveryCursor.moveToFirst();
        } finally {
            if (deliveryCursor != null) deliveryCursor.close();
        }
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/DataStore.java
================================================
package pro.dbro.ble.data;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.util.List;

import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.MessageCollection;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.protocol.IdentityPacket;
import pro.dbro.ble.protocol.MessagePacket;
import pro.dbro.ble.protocol.Protocol;

/**
 * Data persistence layer. Any data storage mechanism
 * needs to implement this interface.
 *
 * Created by davidbrodsky on 10/20/14.
 */
public abstract class DataStore {

    protected Context mContext;

    public DataStore(@NonNull Context context) {
        mContext = context.getApplicationContext();
    }

    public abstract void markMessageDeliveredToPeer(@NonNull MessagePacket message, @NonNull IdentityPacket recipient);

    public abstract void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity);

    public abstract Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol);

    public abstract Peer getPrimaryLocalPeer();

    public abstract List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages);

    public abstract List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxMessages);

    public abstract MessageCollection getRecentMessages();

    public abstract MessageCollection getRecentMessagesByPeer(@NonNull Peer author);

    public abstract Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket identityPacket);

    public abstract Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket);

    public abstract Message getMessageBySignature(@NonNull byte[] signature);

    public abstract Message getMessageById(int id);

    public abstract Peer getPeerByPubKey(@NonNull byte[] publicKey);

    public abstract Peer getPeerById(int id);

    public abstract int countPeers();

    public abstract int countMessagesPassed();

}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/ChatContentProvider.java
================================================
package pro.dbro.ble.data.model;

import android.net.Uri;

import net.simonvt.schematic.annotation.ContentProvider;
import net.simonvt.schematic.annotation.ContentUri;
import net.simonvt.schematic.annotation.TableEndpoint;

/**
 * ContentProvider definition. This defines a familiar API
 * for Android framework components to utilize.
 *
 * Created by davidbrodsky on 7/28/14.
 */
@ContentProvider(authority = ChatContentProvider.AUTHORITY, database = ChatDatabase.class)
public final class ChatContentProvider {

    public static final String AUTHORITY      = "pro.dbro.ble.chatprovider";
    private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);

    private static Uri buildUri(String... paths) {
        Uri.Builder builder = BASE_CONTENT_URI.buildUpon();
        for (String path : paths) {
            builder.appendPath(path);
        }
        return builder.build();
    }

    /** Peer API **/

    @TableEndpoint(table = ChatDatabase.PEERS)
    public static class Peers {

        private static final String ENDPOINT = "peers";

        @ContentUri(
                path = ENDPOINT,
                type = "vnd.android.cursor.dir/list",
                defaultSort = PeerTable.alias + " ASC")
        public static final Uri PEERS = buildUri(ENDPOINT);
    }

    /** Messages API **/

    @TableEndpoint(table = ChatDatabase.MESSAGES)
    public static class Messages {

        private static final String ENDPOINT = "msgs";

        @ContentUri(
                path = ENDPOINT,
                type = "vnd.android.cursor.dir/list",
                defaultSort = MessageTable.authoredDate + " ASC")
        public static final Uri MESSAGES = buildUri(ENDPOINT);

    }

    /** MessageDelivery API **/

    @TableEndpoint(table = ChatDatabase.DELIVERED_MESSAGES)
    public static class MessageDeliveries {

        private static final String ENDPOINT = "message_deliveries";

        @ContentUri(
                path = ENDPOINT,
                type = "vnd.android.cursor.dir/list",
                defaultSort = MessageDeliveryTable.messageId + " ASC")
        public static final Uri MESSAGE_DELIVERIES = buildUri(ENDPOINT);

    }

    /** IdentityDelivery API **/

    @TableEndpoint(table = ChatDatabase.DELIVERED_IDENTITIES)
    public static class IdentityDeliveries {

        private static final String ENDPOINT = "identity_deliveries";

        @ContentUri(
                path = ENDPOINT,
                type = "vnd.android.cursor.dir/list",
                defaultSort = IdentityDeliveryTable.peerRecipientId + " ASC")
        public static final Uri IDENTITY_DELIVERIES = buildUri(ENDPOINT);

    }

}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/ChatDatabase.java
================================================
package pro.dbro.ble.data.model;

import net.simonvt.schematic.annotation.Database;
import net.simonvt.schematic.annotation.Table;

/**
 * SQL Database definition.
 *
 * Created by davidbrodsky on 7/28/14.
 */
@Database(version = ChatDatabase.DATABASE_VERSION)
public class ChatDatabase {

    public static final int DATABASE_VERSION = 1;

    /** Table Definition                Reference Name                                     SQL Tablename */
    @Table(PeerTable.class)             public static final String  PEERS                = "peers";
    @Table(MessageTable.class)          public static final String  MESSAGES             = "msgs";
    @Table(MessageDeliveryTable.class)  public static final String  DELIVERED_MESSAGES   = "m_dlvry";
    @Table(IdentityDeliveryTable.class) public static final String  DELIVERED_IDENTITIES = "p_dlvry";
}

================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/CursorModel.java
================================================
package pro.dbro.ble.data.model;

import android.database.Cursor;
import android.support.annotation.NonNull;

import java.io.Closeable;

/**
 * Created by davidbrodsky on 10/20/14.
 */
public abstract class CursorModel implements Closeable{

    protected Cursor mCursor;

    /**
     * Use this constructor if you intend to immediately access model data.
     * @param cursor A cursor that is already moved to the row corresponding to the desired model instance
     */
    public CursorModel(@NonNull Cursor cursor) {
        mCursor = cursor;
    }

    public Cursor getCursor() {
        return mCursor;
    }

    @Override
    public void close() {
        if (mCursor != null) {
            mCursor.close();
        }
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/DataUtil.java
================================================
package pro.dbro.ble.data.model;

import java.text.SimpleDateFormat;
import java.util.Locale;

/**
 * Utilities for converting between Java and Database friendly types
 *
 * Created by davidbrodsky on 10/13/14.
 */
public class DataUtil {

    public static SimpleDateFormat storedDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);

    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

    /**
     * When we query rows by a BLOB column we must
     * convert the BLOB to its String hex form
     * see:
     * http://www.sqlite.org/lang_expr.html#litvalue
     */
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        String rawHex = new String(hexChars);
        String blobLiteral = "X'" + rawHex + "'";
        return blobLiteral;
    }

}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/IdentityDeliveryTable.java
================================================
package pro.dbro.ble.data.model;

import net.simonvt.schematic.annotation.AutoIncrement;
import net.simonvt.schematic.annotation.DataType;
import net.simonvt.schematic.annotation.NotNull;
import net.simonvt.schematic.annotation.PrimaryKey;

import static net.simonvt.schematic.annotation.DataType.Type.INTEGER;

/**
 * Used to avoid sending a single identity to a particular client multiple times
 *
 * Created by davidbrodsky on 7/28/14.
 */
public interface IdentityDeliveryTable {

    /** SQL type        Modifiers                   Reference Name            SQL Column Name */
    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id                  = "_id";
    @DataType(INTEGER)  @NotNull                    String peerRecipientId     = "pr_id";
    @DataType(INTEGER)  @NotNull                    String peerPayloadId       = "pp_id";
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/Message.java
================================================
package pro.dbro.ble.data.model;

import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.text.ParseException;
import java.util.Date;

import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.protocol.MessagePacket;

/**
 * A thin model around a {@link android.database.Cursor}
 * that lazy-loads attributes as needed. As such, do
 * not to close the cursor fed to this class's constructor.
 * Instead call {@link #close}
 * <p/>
 * Created by davidbrodsky on 10/12/14.
 */
public class Message extends CursorModel {

    public Message(@NonNull Cursor cursor) {
        super(cursor);

    }

    public int getId() {
        return mCursor.getInt(mCursor.getColumnIndex(MessageTable.id));
    }

    public String getBody() {
        return mCursor.getString(mCursor.getColumnIndex(MessageTable.body));
    }

    public Date getAuthoredDate() {
        try {
            return DataUtil.storedDateFormatter.parse(mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));
        } catch (ParseException e) {
            e.printStackTrace();
            return null;
        }
    }

    public byte[] getPublicKey(DataStore dataStore) {
        return getSender(dataStore).getIdentity().publicKey;
    }

    public byte[] getSignature() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.signature));
    }

    public byte[] getReplySignature() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.replySig));
    }

    public byte[] getRawPacket() {
        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.rawPacket));
    }

    @Nullable
    public Peer getSender(DataStore dataStore) {
        return dataStore.getPeerById(mCursor.getInt(mCursor.getColumnIndex(MessageTable.peerId)));
    }

    @Nullable
    public MessagePacket getProtocolMessage(DataStore dataStore) {
        return new MessagePacket(
                getSender(dataStore).getIdentity(),
                getSignature(),
                getReplySignature(),
                getBody(),
                getRawPacket(),
                getAuthoredDate());

    }

    @Nullable
    public Date getRelativeReceivedDate() {
        try {
            return DataUtil.storedDateFormatter.parse(
                    mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));
        } catch (ParseException e) {
            return null;
        }
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/MessageCollection.java
================================================
package pro.dbro.ble.data.model;

import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;


/**
 * Created by davidbrodsky on 10/20/14.
 */
public class MessageCollection extends CursorModel {

    public MessageCollection(@NonNull Cursor cursor) {
        super(cursor);
    }

    @Nullable
    public Message getMessageAtPosition(int position) {
        boolean success = mCursor.move(position);
        if (success)
            return new Message(mCursor);
        return null;
    }

    public Cursor getCursor() {
        return mCursor;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/MessageDeliveryTable.java
================================================
package pro.dbro.ble.data.model;

import net.simonvt.schematic.annotation.AutoIncrement;
import net.simonvt.schematic.annotation.DataType;
import net.simonvt.schematic.annotation.NotNull;
import net.simonvt.schematic.annotation.PrimaryKey;

import static net.simonvt.schematic.annotation.DataType.Type.INTEGER;

/**
 * Used to avoid sending a single messages to a particular client multiple times
 *
 * Created by davidbrodsky on 7/28/14.
 */
public interface MessageDeliveryTable {

    /** SQL type        Modifiers                   Reference Name            SQL Column Name */
    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id                  = "_id";
    @DataType(INTEGER)  @NotNull                    String messageId           = "m_id";
    @DataType(INTEGER)  @NotNull                    String peerId              = "p_id";
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/MessageTable.java
================================================
package pro.dbro.ble.data.model;

import net.simonvt.schematic.annotation.AutoIncrement;
import net.simonvt.schematic.annotation.DataType;
import net.simonvt.schematic.annotation.NotNull;
import net.simonvt.schematic.annotation.PrimaryKey;

import static net.simonvt.schematic.annotation.DataType.Type.BLOB;
import static net.simonvt.schematic.annotation.DataType.Type.INTEGER;
import static net.simonvt.schematic.annotation.DataType.Type.TEXT;

/**
 * Created by davidbrodsky on 7/28/14.
 */
public interface MessageTable {

    /** SQL type        Modifiers                   Reference Name            SQL Column Name */
    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id               = "_id";
    @DataType(TEXT)     @NotNull                    String body             = "body";
    @DataType(INTEGER)                              String peerId           = "p_id";
    @DataType(TEXT)                                 String authoredDate     = "author_date";
    @DataType(TEXT)                                 String receivedDate     = "recv_date";
    @DataType(BLOB)                                 String signature        = "sig";
    @DataType(BLOB)                                 String replySig         = "r_sig";
    @DataType(BLOB)                                 String rawPacket        = "pkt";
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/Peer.java
================================================
package pro.dbro.ble.data.model;

import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.text.ParseException;
import java.util.Date;

import pro.dbro.ble.protocol.IdentityPacket;
import pro.dbro.ble.protocol.OwnedIdentityPacket;

/**
 * Created by davidbrodsky on 10/12/14.
 */
public class Peer {

    private int mId;
    private byte[] mPublicKey;
    private byte[] mSecretKey;
    private String mAlias;
    private Date mLastSeen;

    private byte[] mRawPkt;


    public Peer(@NonNull Cursor cursor) {
        mId = cursor.getInt(cursor.getColumnIndex(PeerTable.id));
        mPublicKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.pubKey));
        mSecretKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.secKey));
        mAlias = cursor.getString(cursor.getColumnIndex(PeerTable.alias));
        mRawPkt = cursor.getBlob(cursor.getColumnIndex(PeerTable.rawPkt));

        try {
            mLastSeen = DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(PeerTable.lastSeenDate)));
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public int getId() {
       return mId;
    }

    public byte[] getPublicKey() {
        return mPublicKey;
    }

    public String getAlias() {
        return mAlias;
    }

    @Nullable
    public Date getLastDateSeen() {
        return mLastSeen;
    }
    /**
     * @return whether this peer represents the application user.
     * e.g: Do we have a secret key
     */
    public boolean isLocalPeer() {
        return mSecretKey != null && mSecretKey.length > 0;
    }

    /**
     * @return a {@link pro.dbro.ble.protocol.OwnedIdentityPacket} for this peer,
     * or an {@link pro.dbro.ble.protocol.IdentityPacket} if this peer is not a user-owned peer.
     * <p/>
     * see {@link #isLocalPeer()}
     */
    public IdentityPacket getIdentity() {
        if (!isLocalPeer()) {
            return new IdentityPacket(mPublicKey, mAlias, mLastSeen, mRawPkt);
        } else {
            return new OwnedIdentityPacket(mSecretKey, mPublicKey, mAlias, mRawPkt);
        }
    }

    @Override
    public boolean equals(Object obj) {

        if(obj == this) return true;
        if(obj == null) return false;

        if (getClass().equals(obj.getClass()))
        {
            final Peer other = (Peer) obj;

            return mId == other.mId;
        }

        return false;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/data/model/PeerTable.java
================================================
package pro.dbro.ble.data.model;

import net.simonvt.schematic.annotation.AutoIncrement;
import net.simonvt.schematic.annotation.DataType;
import net.simonvt.schematic.annotation.NotNull;
import net.simonvt.schematic.annotation.PrimaryKey;

import static net.simonvt.schematic.annotation.DataType.Type.BLOB;
import static net.simonvt.schematic.annotation.DataType.Type.INTEGER;
import static net.simonvt.schematic.annotation.DataType.Type.TEXT;

/**
 * Created by davidbrodsky on 7/28/14.
 */
public interface PeerTable {

    /** SQL type        Modifiers                   Reference Name            SQL Column Name */
    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id               = "_id";
    @DataType(TEXT)                                 String alias            = "alias";
    @DataType(TEXT)     @NotNull                    String lastSeenDate     = "last_seen";
    @DataType(BLOB)     @NotNull                    String pubKey           = "pk";
    @DataType(BLOB)                                 String secKey           = "sk";
    @DataType(BLOB)                                 String rawPkt           = "pkt";

}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/BLEProtocol.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;

import pro.dbro.ble.crypto.SodiumShaker;

/**
 * Created by davidbrodsky on 10/14/14.
 */
public class BLEProtocol implements Protocol {
    public static final String TAG = "ChatProtocol";

    // <editor-fold desc="Public API">
    /** Bluetooth LE Mesh Chat Protocol Version */
    public static final byte VERSION = 0x01;

    /** Identity */
    public static final int NODATA_RESPONSE_LENGTH     = 106;  // bytes
    public static final int MESSAGE_RESPONSE_LENGTH    = 310;  // bytes
    public static final int IDENTITY_RESPONSE_LENGTH   = 141;  // bytes
    public static final int MESSAGE_BODY_LENGTH        = 140;  // bytes
    public static final int ALIAS_LENGTH               = 35;   // bytes

    private static final ByteBuffer sTimeStampBuffer = ByteBuffer.allocate(Long.SIZE / 8);

    static {
        sTimeStampBuffer.order(ByteOrder.LITTLE_ENDIAN);
    }

    /** Outgoing
     *
     * Create raw transmission data from protocol Objects
    */

    // TODO : Make this API consistent. Either all byte[] or all Packet structures
    // The below method would be unnecessary if we ensured the rawPacket attribute
    // was created on OwnedIdentityPacket's construction. We should only ever have
    // to serialize our own identity. Every other identity is received serialized.
    @Nullable
    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity) {
        // Protocol version 1
        //[[version=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]
        try {
            byte[] identity = new byte[IDENTITY_RESPONSE_LENGTH];
            int writeIndex = 0;
            writeIndex += addVersionToBuffer(identity, writeIndex);
            writeIndex += addTypeToBuffer(identity, IdentityPacket.TYPE, writeIndex);
            writeIndex += addTimestampToBuffer(identity, writeIndex);
            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, identity, writeIndex);
            writeIndex += addAliasToBuffer(ownedIdentity.alias, identity, writeIndex);
            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, identity, writeIndex);

            if (writeIndex != IDENTITY_RESPONSE_LENGTH)
                throw new IllegalStateException("Generated Identity does not match expected length");

            return identity;
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @Nullable
    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body) {
        // Protocol version 1
        //[[version=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]
        try {
            byte[] message = new byte[MESSAGE_RESPONSE_LENGTH];
            int writeIndex = 0;
            writeIndex += addVersionToBuffer(message, writeIndex);
            writeIndex += addTypeToBuffer(message, MessagePacket.TYPE, writeIndex);
            writeIndex += addTimestampToBuffer(message, writeIndex);
            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, message, writeIndex);
            writeIndex += addMessageBodyToBuffer(body, message, writeIndex);
            writeIndex += 64; // Empty reply_signature
            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, message, writeIndex);

            if (writeIndex != MESSAGE_RESPONSE_LENGTH)
                throw new IllegalStateException("Generated Message does not match expected length");

            return deserializeMessageWithIdentity(message, ownedIdentity);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @NonNull
    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity) {
        byte[] noDataPkt = new byte[NODATA_RESPONSE_LENGTH];
        int writeIndex = 0;
        writeIndex += addVersionToBuffer(noDataPkt, writeIndex);
        writeIndex += addTypeToBuffer(noDataPkt, NoDataPacket.TYPE, writeIndex);
        writeIndex += addTimestampToBuffer(noDataPkt, writeIndex);
        writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, noDataPkt, writeIndex);
        writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, noDataPkt, writeIndex);

        if (writeIndex != NODATA_RESPONSE_LENGTH)
            throw new IllegalStateException("Generated Message does not match expected length");

        return deserializeNoDataPacket(noDataPkt);
    }

    /** Incoming
     *
     * Produce protocol Objects from raw transmission data
     */

    @Nullable
    public IdentityPacket deserializeIdentity(@NonNull byte[] identity) {
        if (identity.length != IDENTITY_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Identity response is %d bytes. Expect %d", identity.length, IDENTITY_RESPONSE_LENGTH));

        // Protocol version 1
        //[[version=1][type=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]
        try {
            int readIndex     = 0;
            byte[] timestamp  = new byte[Long.SIZE / 8];
            byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
            byte[] alias      = new byte[ALIAS_LENGTH];
            byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] signedData = new byte[IDENTITY_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

            readIndex += assertBufferVersion(identity, readIndex);
            readIndex += assertBufferType(identity, IdentityPacket.TYPE, readIndex);
            readIndex += getBytesFromBuffer(identity, timestamp, readIndex);
            readIndex += getBytesFromBuffer(identity, public_key, readIndex);
            readIndex += getBytesFromBuffer(identity, alias, readIndex);
            readIndex += getBytesFromBuffer(identity, signature, readIndex);

            System.arraycopy(identity, 0, signedData, 0, signedData.length);
            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
            if (!validSignature)
                throw new IllegalStateException("Identity signature does not match content!");

            return new IdentityPacket(public_key, new String(alias, "UTF-8"), getDateFromTimestampBuffer(timestamp), identity);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @Nullable
    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @NonNull IdentityPacket identity) {
        if (message.length != MESSAGE_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Message response is illegal length. Got %d expected %d", message.length, MESSAGE_RESPONSE_LENGTH));

        // TODO Don't duplicate this code between de
        MessagePacket messageWithoutIdentity = deserializeMessage(message);
        MessagePacket messageWithIdentity = null;

        if (messageWithoutIdentity != null) {
            messageWithIdentity = MessagePacket.attachIdentityToMessage(messageWithoutIdentity, identity);
        }
        return messageWithIdentity;
    }

    @Nullable
    public MessagePacket deserializeMessage(@NonNull byte[] message) {
        if (message.length != MESSAGE_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("Message response is illegal length. Got %d expected %d", message.length, MESSAGE_RESPONSE_LENGTH));

        // Protocol version 1
        //[[version=1][type=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]
        try {
            int readIndex          = 0;
            byte[] timestamp       = new byte[Long.SIZE / 8];
            byte[] public_key      = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
            byte[] body            = new byte[MESSAGE_BODY_LENGTH];
            byte[] signature       = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] replySignature  = new byte[SodiumShaker.crypto_sign_BYTES];
            byte[] signedData      = new byte[MESSAGE_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

            readIndex += assertBufferVersion(message, readIndex);
            readIndex += assertBufferType(message, MessagePacket.TYPE, readIndex);
            readIndex += getBytesFromBuffer(message, timestamp, readIndex);
            readIndex += getBytesFromBuffer(message, public_key, readIndex);
            readIndex += getBytesFromBuffer(message, body, readIndex);
            readIndex += getBytesFromBuffer(message, replySignature, readIndex);
            readIndex += getBytesFromBuffer(message, signature, readIndex);

            System.arraycopy(message, 0, signedData, 0, signedData.length);
            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
            if (!validSignature)
                throw new IllegalStateException("Message signature does not match content!");

            return new MessagePacket(public_key, signature, replySignature, getDateFromTimestampBuffer(timestamp), new String(body, "UTF-8"), message);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?");
            e.printStackTrace();
        }
        return null;
    }

    @NonNull
    public NoDataPacket deserializeNoDataPacket(@NonNull byte[] noDataPkt) {
        if (noDataPkt.length != NODATA_RESPONSE_LENGTH)
            throw new IllegalArgumentException(String.format("NoData response is %d bytes. Expect %d", noDataPkt.length, IDENTITY_RESPONSE_LENGTH));

        // Protocol version 1
        // [[version=1][type=1][timestamp=8][sender_public_key=32]][signature=64]
        int readIndex     = 0;
        byte[] timestamp  = new byte[Long.SIZE / 8];
        byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];
        byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];
        byte[] signedData = new byte[NODATA_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];

        readIndex += assertBufferVersion(noDataPkt, readIndex);
        readIndex += assertBufferType(noDataPkt, NoDataPacket.TYPE, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, timestamp, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, public_key, readIndex);
        readIndex += getBytesFromBuffer(noDataPkt, signature, readIndex);

        System.arraycopy(noDataPkt, 0, signedData, 0, signedData.length);
        boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);
        if (!validSignature)
            throw new IllegalStateException("NoData signature does not match content!");

        return new NoDataPacket(public_key, getDateFromTimestampBuffer(timestamp), signature, noDataPkt);
    }


    public byte getPacketType(@NonNull byte[] message) {
        byte[] type = new byte[1];
        getTypeFromBuffer(message, type, 1);
        return type[0];
    }

    // </editor-fold desc="Public API">

    // <editor-fold desc="Private API">

    private static int addVersionToBuffer(@NonNull byte[] input, int offset) {
        int bytesToWrite = 1;
        assertBufferLength(input, offset + bytesToWrite);

        input[offset] = VERSION;
        return bytesToWrite;
    }

    private static int getVersionFromBuffer(@NonNull byte[] input, @NonNull byte[] version, int offset) {
        int bytesToRead = 1;
        assertBufferLength(input, offset + bytesToRead);
        version[0] = input[offset];
        return bytesToRead;
    }

    private static int addTypeToBuffer(@NonNull byte[] input, byte type, int offset) {
        int bytesToWrite = 1;
        assertBufferLength(input, offset + bytesToWrite);

        input[offset] = type;
        return bytesToWrite;
    }

    private static int getTypeFromBuffer(@NonNull byte[] input, @NonNull byte[] type, int offset) {
        int bytesToRead = 1;
        assertBufferLength(input, offset + bytesToRead);
        type[0] = input[offset];
        return bytesToRead;
    }

    private static int addTimestampToBuffer(@NonNull byte[] input, int offset) {
        synchronized (sTimeStampBuffer) {
            int bytesToWrite = Long.SIZE / 8;
            assertBufferLength(input, offset + bytesToWrite);

            long unixTime64 = System.currentTimeMillis();
            sTimeStampBuffer.rewind();
            sTimeStampBuffer.putLong(unixTime64);
            System.arraycopy(sTimeStampBuffer.array(), 0, input, offset, bytesToWrite);
            return bytesToWrite;
        }
    }

    private static int addPublicKeyToBuffer(@NonNull byte[] public_key, @NonNull byte[] input, int offset) {
        int bytesToWrite = public_key.length;
        assertBufferLength(input, offset + bytesToWrite);

        System.arraycopy(public_key, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int addAliasToBuffer(@NonNull String alias, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {
        int bytesToWrite = ALIAS_LENGTH;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] aliasAsBytes = alias.getBytes("UTF-8");
        byte[] paddedAliasAsBytes = new byte[ALIAS_LENGTH];

        truncateOrPadTextBuffer(aliasAsBytes, paddedAliasAsBytes);

        System.arraycopy(paddedAliasAsBytes, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int addMessageBodyToBuffer(@NonNull String body, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {
        int bytesToWrite = MESSAGE_BODY_LENGTH;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] bodyAsBytes = body.getBytes("UTF-8");
        byte[] paddedBodyAsBytes = new byte[MESSAGE_BODY_LENGTH];

        truncateOrPadTextBuffer(bodyAsBytes, paddedBodyAsBytes);

        System.arraycopy(paddedBodyAsBytes, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    private static int getBytesFromBuffer(@NonNull byte[] input, @NonNull byte[] output, int offset) {
        int bytesToRead = output.length;
        assertBufferLength(input, offset + bytesToRead);

        System.arraycopy(input, offset, output, 0, bytesToRead);
        return bytesToRead;
    }

    /**
     * Generate signature for input from the first byte until the offset byte. Append signature to input after offset byte.
     */
    private static int addSignatureToBuffer(@NonNull byte[] secret_key, @NonNull byte[] input, int offset) {
        int bytesToWrite = SodiumShaker.crypto_sign_BYTES;
        assertBufferLength(input, offset + bytesToWrite);

        byte[] signature = SodiumShaker.generateSignatureForMessage(secret_key, input, offset);

        System.arraycopy(signature, 0, input, offset, bytesToWrite);
        return bytesToWrite;
    }

    /** Utility */

    /**
     * Truncates or pads input to fit precisely inside output.
     * After this call output will contain the truncated or padded input
     */
    private static void truncateOrPadTextBuffer(byte[] input, byte[] output) {
        System.arraycopy(input, 0, output, 0, Math.min(input.length, output.length));
        if (input.length < output.length) {
            for (int x = input.length; x < output.length; x++) {
                output[x] = 0x20; // UTF-8 space
            }
        }
    }

    private static void assertBufferLength(byte[] input, int minimumLength) {
        if (input.length < minimumLength)
            throw new IllegalArgumentException(String.format("Operation requires input buffer length %d. Actual: %d", minimumLength, input.length));
    }

    private static int assertBufferVersion(byte[] input, int offset) {
        byte[] version = new byte[1];
        getVersionFromBuffer(input, version, offset);

        if (version[0] != VERSION)
            throw new IllegalStateException(String.format("Response is for an unknown protocol version. Got %d. Expected %d", version[0], VERSION));
        return 1;
    }

    private static int assertBufferType(byte[] input, byte expectedType, int offset) {
        byte[] type = new byte[1];
        getTypeFromBuffer(input, type, offset);

        if (type[0] != expectedType)
            throw new IllegalStateException(String.format("Response is for an unexpected message type. Got %d. Expected %d", type[0], expectedType));
        return 1;
    }

    @Nullable
    private static Date getDateFromTimestampBuffer(byte[] timestamp) {
        synchronized (sTimeStampBuffer) {
            sTimeStampBuffer.clear();
            sTimeStampBuffer.put(timestamp);
            sTimeStampBuffer.rewind();
            // TODO: Test if flip needed
            return new Date(sTimeStampBuffer.getLong());
        }
    }

    // </editor-fold desc="Private API">
}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/IdentityPacket.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.util.Date;

/**
 * An identity for a remote peer
 * Created by davidbrodsky on 10/13/14.
 */
public class IdentityPacket {
    public static final byte TYPE = 0x01;

    public final byte[] publicKey;
    public final Date   dateSeen;
    public final String alias;
    public final byte[] rawPacket;

    public IdentityPacket(@NonNull final byte[] publicKey, @Nullable String alias, @NonNull Date dateSeen,
                          @NonNull final byte[] rawPacket) {
        // dateSeen is allowed null because it's meaningless for OwnedIdentities
        this.publicKey  = publicKey;
        this.alias      = alias == null ? null : alias.trim();
        this.dateSeen   = dateSeen;
        this.rawPacket  = rawPacket;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/MessagePacket.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;

import java.util.Date;

/**
 * Created by davidbrodsky on 10/15/14.
 */
public class MessagePacket {
    public static final byte TYPE = 0x02;

    final public IdentityPacket sender;
    final public String body;
    final public Date authoredDate;
    final public byte[] signature;
    final public byte[] replySig;
    final public byte[] rawPacket;

    /** Incoming */
    public MessagePacket(@NonNull final byte[] publicKey,
                         @NonNull byte[] signature,
                         @NonNull byte[] replySig,
                         @NonNull Date authoredDate,
                         @NonNull String body,
                         @NonNull byte[] rawPacket) {

        this.body         = body;
        this.signature    = signature;
        this.replySig     = replySig;
        this.rawPacket    = rawPacket;
        this.authoredDate = authoredDate;
        sender            = new IdentityPacket(publicKey, null, null, null); // We don't have the sender's full identity response
    }

    public static MessagePacket attachIdentityToMessage(@NonNull MessagePacket message, @NonNull IdentityPacket identity) {
        return new MessagePacket(identity, message.signature, message.replySig, message.body, message.rawPacket, message.authoredDate);
    }

    /** Outgoing */
    public MessagePacket(@NonNull IdentityPacket sender,
                         @NonNull byte[] signature,
                         @NonNull byte[] replySig,
                         @NonNull String body,
                         @NonNull byte[] rawPacket,
                         @NonNull Date authoredDate) {

        this.body         = body.trim();
        this.signature    = signature;
        this.replySig     = replySig;
        this.rawPacket    = rawPacket;
        this.authoredDate = authoredDate;
        this.sender       = sender;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/NoDataPacket.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;

import java.util.Date;

/**
 * Created by davidbrodsky on 10/15/14.
 *
 */
public class NoDataPacket {
    public static final byte TYPE = 0x03;

    final public byte[] publicKey;
    final public Date authoredDate;
    final public byte[] signature;
    final public byte[] rawPacket;

    public NoDataPacket(@NonNull final byte[] publicKey,
                        @NonNull Date authoredDate,
                        @NonNull byte[] signature,
                        @NonNull byte[] rawPacket) {

        this.publicKey    = publicKey;
        this.signature    = signature;
        this.rawPacket    = rawPacket;
        this.authoredDate = authoredDate;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/OwnedIdentityPacket.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;

/**
 * An Identity for the local peer
 * Created by davidbrodsky on 10/13/14.
 */
public class OwnedIdentityPacket extends IdentityPacket {

    public final byte[] secretKey;

    public OwnedIdentityPacket(@NonNull final byte[] secretKey, @NonNull final byte[] publicKey,
                               @NonNull String alias, byte[] rawPacket) {
        super(publicKey, alias, null, rawPacket);
        this.secretKey = secretKey;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/protocol/Protocol.java
================================================
package pro.dbro.ble.protocol;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

/**
 * Created by davidbrodsky on 10/20/14.
 */
public interface Protocol {

    /** Outgoing
     *
     * Serialize Protocol Objects to raw transmission data
    **/

    // TODO Decide on a consistent API here
    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity);

    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body);

    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity);

    /** Incoming
     *
     * Deserialize raw transmission data into Protocol Objects
     */

    public IdentityPacket deserializeIdentity(@NonNull byte[] identity);

    /** Deserialize a message where the author identity is known */
    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @Nullable IdentityPacket identity);

    /** Deserialize a message where the author identity is not known */
    public MessagePacket deserializeMessage(@NonNull byte[] message);

    public byte getPacketType(@NonNull byte[] message);

}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/Notification.java
================================================
package pro.dbro.ble.ui;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.view.View;

import java.util.ArrayList;

import im.delight.android.identicons.SymmetricIdenticon;
import pro.dbro.ble.R;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.ui.activities.MainActivity;

/**
 * Created by davidbrodsky on 11/14/14.
 */
public class Notification {

    /** Notification Ids */
    private static final int MESSAGE_NOTIFICATION_ID = 1;
    private static final int PEER_AVAILABLE_NOTIFICATION_ID = 2;

    private static final int MAX_MESSAGES_TO_SHOW = 6;

    private static final ArrayList<String> sNotificationInboxItems = new ArrayList<>(MAX_MESSAGES_TO_SHOW + 1);

    // <editor-fold desc="Public API">

    /**
     * Display a notification representing peer being available, or remove any indicating such
     * if isAvailable is false.
     *
     * Does not call peer.close()
     */
    public static void displayPeerAvailableNotification(@NonNull Context context, @NonNull Peer peer, boolean isAvailable) {
        NotificationManager mNotificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        if (!isAvailable) {
            mNotificationManager.cancel(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID);
            return;
        }
        if (peer.getAlias() == null) return; // TODO : Notify of peers without alias?

        String title = String.format("%s is nearby", peer.getAlias());

        Intent resultIntent = new Intent(context, MainActivity.class);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentTitle(title);
        builder.setContentIntent(makePendingIntent(context, resultIntent));
        builder.setContentText(context.getString(R.string.notification_touch_to_chat));

        mNotificationManager.notify(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID, builder.build());
    }

    /**
     * Display a notification representing a new received message. Multiple calls to this method are displayed as a single
     * notification, showing a preview of the last MAX_MESSAGES_TO_SHOW messages.
     *
     * Does not call message.close() or sender.close()
     */
    public static void displayMessageNotification(@NonNull Context context, @NonNull Message message, @Nullable Peer sender) {
        StringBuilder nBuilder = new StringBuilder();
        if (sender != null && sender.getAlias() != null) {
            nBuilder.append(sender.getAlias());
            nBuilder.append(": ");
        }
        nBuilder.append(message.getBody().length() > 80 ?
                            message.getBody().substring(0, 80) + "..." :
                            message.getBody());
        sNotificationInboxItems.add(nBuilder.toString());
        if (sNotificationInboxItems.size() > MAX_MESSAGES_TO_SHOW) sNotificationInboxItems.remove(sNotificationInboxItems.size()-1);

        Intent resultIntent = new Intent(context, MainActivity.class);

        NotificationCompat.InboxStyle inboxStyle =
                new NotificationCompat.InboxStyle();
        inboxStyle.setBigContentTitle(context.getString(R.string.notification_new_messages));

        for (String inboxItem : sNotificationInboxItems) {
            inboxStyle.addLine(inboxItem);
        }

        SymmetricIdenticon identicon = new SymmetricIdenticon(context);
        identicon.show(new String(sender.getPublicKey()));

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
        builder.setContentTitle(context.getString(R.string.notification_new_messages));
        builder.setLargeIcon(loadBitmapFromView(identicon, 640, 480));
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentIntent(makePendingIntent(context, resultIntent));
        builder.setStyle(inboxStyle);
        builder.setContentText(sNotificationInboxItems.get(0));
        builder.setAutoCancel(true);
        builder.setCategory(NotificationCompat.CATEGORY_MESSAGE);
        builder.setVibrate(new long[] { 500, 500, 500, 500});

        NotificationManager mNotificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        mNotificationManager.notify(MESSAGE_NOTIFICATION_ID, builder.build());
    }

    // </editor-fold desc="Public API">

    // <editor-fold desc="Private API">

    private static PendingIntent makePendingIntent(@NonNull Context context, @NonNull Intent resultIntent) {
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        // Adds the back stack
        stackBuilder.addParentStack(MainActivity.class);
        // Adds the Intent to the top of the stack
        stackBuilder.addNextIntent(resultIntent);
        // Gets a PendingIntent containing the entire back stack
        return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    public static Bitmap loadBitmapFromView(View v, int width, int height) {

        int measuredWidth = View.MeasureSpec.makeMeasureSpec(width,
                View.MeasureSpec.EXACTLY);
        int measuredHeight = View.MeasureSpec.makeMeasureSpec(height,
                View.MeasureSpec.EXACTLY);
        v.measure(measuredWidth, measuredHeight);
        v.layout(0, 0, v.getMeasuredWidth(),v.getMeasuredHeight());

        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        v.draw(c);
        return b;
    }

    // </editor-fold desc="Private API">
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/activities/LogConsumer.java
================================================
package pro.dbro.ble.ui.activities;

/**
 * Created by davidbrodsky on 10/11/14.
 */
public interface LogConsumer {
    public void onLogEvent(String event);
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/activities/MainActivity.java
================================================
package pro.dbro.ble.ui.activities;

import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.FragmentManager;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.Toolbar;
import android.transition.Slide;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;

import com.nispok.snackbar.Snackbar;

import java.util.ArrayList;
import java.util.Arrays;

import butterknife.ButterKnife;
import butterknife.InjectView;
import hugo.weaving.DebugLog;
import im.delight.android.identicons.SymmetricIdenticon;
import pro.dbro.airshare.app.AirShareService;
import pro.dbro.airshare.app.ui.AirShareFragment;
import pro.dbro.ble.ChatClient;
import pro.dbro.ble.ChatPeerFlow;
import pro.dbro.ble.PrefsManager;
import pro.dbro.ble.R;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.protocol.OwnedIdentityPacket;
import pro.dbro.ble.ui.Notification;
import pro.dbro.ble.ui.adapter.StatusArrayAdapter;
import pro.dbro.ble.ui.fragment.MessagingFragment;
import pro.dbro.ble.ui.fragment.ProfileFragment;
import pro.dbro.ble.ui.fragment.WelcomeFragment;
import timber.log.Timber;

public class MainActivity extends AppCompatActivity implements LogConsumer,
        WelcomeFragment.WelcomeFragmentCallback,
        AirShareFragment.Callback,
        MessagingFragment.ChatFragmentCallback, ChatClient.Callback {

    public static final String TAG = "MainActivity";

    private ActionBarDrawerToggle mDrawerToggle;
    private MessagingFragment mMessagingFragment;
    private OwnedIdentityPacket mUserIdentity;

    private ChatClient mClient;
    private AirShareFragment mAirShareFragment;

    private Palette mPalette;

//    private PeerAdapter mPeerAdapter;

    @InjectView(R.id.status_spinner)
    Spinner mStatusSpinner;

    @InjectView(R.id.log)
    TextView mLogView;

//    @InjectView(R.id.peer_recyclerview)
//    RecyclerView mPeerRecyclerView;

    @InjectView(R.id.toolbar)
    Toolbar mToolbar;

    @InjectView(R.id.my_drawer_layout)
    DrawerLayout mDrawer;

    @InjectView(R.id.msg_pass_count)
    TextView mMessagesPassedCount;

    @InjectView(R.id.peers_met_count)
    TextView mPeersMetCount;

    @InjectView(R.id.profile_identicon)
    SymmetricIdenticon mProfileIdenticon;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);

        mClient = new ChatClient(this);

//        mLogView.setOnLongClickListener(new View.OnLongClickListener() {
//            @Override
//            public boolean onLongClick(View view) {
//                mLogView.setText("");
//                return false;
//            }
//        });

        mStatusSpinner.setAdapter(new StatusArrayAdapter(this, new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.status_options)))));
        mStatusSpinner.setEnabled(false);
        mStatusSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                switch (position) {

                    case 0: // Always online
                        mClient.makeAvailable();
                        mAirShareFragment.setShouldServiceContinueInBackground(true);
                        break;

                    case 1: // Online when using app
                        mClient.makeAvailable();
                        mAirShareFragment.setShouldServiceContinueInBackground(false);
                        break;

                    case 2: // Offline
                        mClient.makeUnavailable();
//                        mPeerAdapter.clearPeers();
                        break;
                }
                PrefsManager.setStatus(MainActivity.this, position);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                // do nothing
            }
        });

        setSupportActionBar(mToolbar);
        setTitle(getString(R.string.public_feed));
        mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
//        mToolbar.setNavigationIcon(R.drawable.ic_drawer);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);

        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawer,
                mToolbar, R.string.drawer_open, R.string.drawer_close) {

            /** Called when a drawer has settled in a completely closed state. */
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
            }

            /** Called when a drawer has settled in a completely open state. */
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                refreshProfileStats();
                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
            }
        };

        // Override ActionB
        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (getSupportFragmentManager().getBackStackEntryCount() == 0)
                    mDrawer.openDrawer(Gravity.START);
                else
                    getSupportFragmentManager().popBackStack();
            }
        });

        mDrawer.setDrawerListener(mDrawerToggle);
        mDrawerToggle.syncState();

        checkUserRegistered();

//        mPeerAdapter = new PeerAdapter(this, new ArrayList<Peer>());
//        mPeerRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
//        mPeerRecyclerView.setAdapter(mPeerAdapter);

        getSupportFragmentManager().addOnBackStackChangedListener(new android.support.v4.app.FragmentManager.OnBackStackChangedListener() {
            @Override
            public void onBackStackChanged() {
                int numEntries = getSupportFragmentManager().getBackStackEntryCount();
                if (numEntries == 0) {
                    mMessagingFragment.animateIn();
                    tintSystemBars(mPalette.getVibrantColor(R.color.primary), mPalette.getDarkVibrantColor(R.color.primaryDark),
                            getResources().getColor(R.color.primary), getResources().getColor(R.color.primaryDark));

                    // Hack animate the drawer icon
                    ValueAnimator drawerAnimator = ValueAnimator.ofFloat(1f, 0f);
                    drawerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mDrawerToggle.onDrawerSlide(null, (Float) animation.getAnimatedValue());
                        }
                    });
                    drawerAnimator.start();
                    setTitle(getString(R.string.public_feed));
                }
            }
        });
    }

    /**
     * Adds the message list fragment and populates
     * the profile navigation drawer with the user profile
     */
    private void revealChatViews() {
        mMessagingFragment = new MessagingFragment();
        mMessagingFragment.setDataStore(mClient.getDataStore());
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container, mMessagingFragment, "messaging")
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .commit();

        mProfileIdenticon.show(new String(mUserIdentity.publicKey));
        ((TextView) findViewById(R.id.profile_name)).setText(mUserIdentity.alias);
    }

    private void refreshProfileStats() {
        mPeersMetCount.setText(String.valueOf(Math.max(0, mClient.getDataStore().countPeers() - 1))); //ignore self
        mMessagesPassedCount.setText(String.valueOf(mClient.getDataStore().countMessagesPassed()));
    }

    /**
     * LogConsumer interface
     */

    @Override
    public void onLogEvent(final String event) {
        /*
        mLogView.post(new Runnable() {
            @Override
            public void run() {
                mLogView.append(event + "\n");

            }
        });
        */
    }

    /**
     * Check if a username has been registered and take appropriate action.
     *
     * If a username has not yet been selected, show WelcomeFragment
     * If a username has been selected, initialize AirShare
     */
    private void checkUserRegistered() {
        Peer localPeer = mClient.getPrimaryLocalPeer();
        if (localPeer != null) {

            // Register ourselves with the AirShare Service, using our own user model's alias
            if (mAirShareFragment == null) {
                mAirShareFragment = AirShareFragment.newInstance(localPeer.getAlias(), ChatClient.AIRSHARE_SERVICE_NAME, this);
                Timber.d("Adding airshare frag");
                getSupportFragmentManager().beginTransaction()
                        .add(mAirShareFragment, "airshare")
                        .commit();
            }

        } else {

            // Show WelcomeFragment to collect the user's desired username
            // will be notified of result via #onNameChosen
            mToolbar.setVisibility(View.GONE);
            getWindow().setStatusBarColor(getResources().getColor(R.color.welcome_status_bar));
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.container, new WelcomeFragment())
                    .commit();
        }
    }

    @Override
    public void onServiceReady(AirShareService.ServiceBinder serviceBinder) {
        mUserIdentity = (OwnedIdentityPacket) mClient.getPrimaryLocalPeer().getIdentity();

        mClient.setAirShareServiceBinder(serviceBinder);
        mClient.setCallback(this);
        mClient.makeAvailable();
        mStatusSpinner.setEnabled(true);
        mStatusSpinner.setSelection(PrefsManager.getStatus(this));
        revealChatViews();
        refreshProfileStats();
    }

    @Override
    public void onFinished(Exception exception) {

    }

    @Override
    public void onMessageSendRequested(String message) {
        mClient.sendPublicMessageFromPrimaryIdentity(message);
    }

    @Override
    public void onMessageSelected(View identictionView, View usernameView, int messageId, int peerId) {
        // Create new fragment to add (Fragment B)
        Peer peer = mClient.getDataStore().getPeerById(peerId);
        if (peer == null) {
            Log.w(TAG, "Could not lookup peer. Cannot show profile");
            return;
        }

        setTitle(peer.getAlias());

//        identictionView.setTransitionName(getString(R.string.identicon_transition_name));
//        usernameView.setTransitionName(getString(R.string.username_transition_name));

        Fragment profileFragment = ProfileFragment.createForPeer(mClient.getDataStore(), peer);

//        final TransitionSet sharedElementTransition = new TransitionSet();
//        sharedElementTransition.addTransition(new ChangeBounds());
//        sharedElementTransition.addTransition(new ChangeTransform());
//        sharedElementTransition.setInterpolator(new AccelerateDecelerateInterpolator());
//        sharedElementTransition.setDuration(200);

        final TransitionSet slideTransition = new TransitionSet();
        slideTransition.addTransition(new Slide());
        slideTransition.setInterpolator(new AccelerateDecelerateInterpolator());
        slideTransition.setDuration(300);

        profileFragment.setEnterTransition(slideTransition);
        profileFragment.setReturnTransition(slideTransition);
//        profileFragment.setSharedElementEnterTransition(sharedElementTransition);
        profileFragment.setAllowEnterTransitionOverlap(false);
        profileFragment.setAllowReturnTransitionOverlap(false);

        // Message fragment performs an exit when Profile is added, and an enter when profile is popped
//        getFragmentManager().findFragmentByTag("messaging").setReenterTransition(slideTransition);
//        getFragmentManager().findFragmentByTag("messaging").setExitTransition(slideTransition);
//        getFragmentManager().findFragmentByTag("messaging").setSharedElementEnterTransition(sharedElementTransition);

        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container, profileFragment)
                .addToBackStack("profile")
//                .addSharedElement(identictionView, getString(R.string.identicon_transition_name))
//                .addSharedElement(usernameView, getString(R.string.username_transition_name))
                .commit();

        Bitmap bitmap = Notification.loadBitmapFromView(identictionView, 100, 100);
        Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
            public void onGenerated(Palette p) {
                mPalette = p;
                tintSystemBars(getResources().getColor(R.color.primary), getResources().getColor(R.color.primaryDark),
                        p.getVibrantColor(R.color.primary), p.getDarkVibrantColor(R.color.primaryDark));

            }
        });

        // Hack animate the drawer icon
        ValueAnimator drawerAnimator = ValueAnimator.ofFloat(0, 1f);
        drawerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mDrawerToggle.onDrawerSlide(null, (Float) animation.getAnimatedValue());
            }
        });
        drawerAnimator.start();
    }

    @Override
    public void onAppPeerStatusUpdated(@NonNull Peer remotePeer, @NonNull ChatPeerFlow.Callback.ConnectionStatus status) {
        Snackbar.with(getApplicationContext())
                .position(Snackbar.SnackbarPosition.TOP)
                .text(String.format("%s %s",
                        remotePeer.getAlias(),
                        status == ChatPeerFlow.Callback.ConnectionStatus.CONNECTED ? "connected" : "disconnected"))
                .show((ViewGroup) findViewById(R.id.container));

//        switch (status) {
//            case CONNECTED:
//                mPeerAdapter.notifyPeerAdded(remotePeer);
//                break;
//
//            case DISCONNECTED:
//                mPeerAdapter.notifyPeerRemoved(remotePeer);
//                break;
//        }
    }

    private void tintSystemBars(final int toolbarFromColor, final int statusbarFromColor,
                                final int toolbarToColor, final int statusbarToColor) {

        ValueAnimator toolbarAnim = ValueAnimator.ofArgb(toolbarFromColor, toolbarToColor);
        ValueAnimator statusbarAnim = ValueAnimator.ofArgb(statusbarFromColor, statusbarToColor);

        statusbarAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                getWindow().setStatusBarColor((Integer) animation.getAnimatedValue());
            }
        });

        toolbarAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                getSupportActionBar().setBackgroundDrawable(new ColorDrawable((Integer) animation.getAnimatedValue()));
            }
        });

        toolbarAnim.setDuration(500).start();
        statusbarAnim.setDuration(500).start();
    }

    @Override
    public void onBackPressed() {
        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
            getSupportFragmentManager().popBackStack();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void onNameChosen(String name) {
        mToolbar.setVisibility(View.VISIBLE);
        getWindow().setStatusBarColor(getResources().getColor(R.color.primaryDark));
        mClient.createPrimaryIdentity(name);
        checkUserRegistered();
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/activities/Util.java
================================================
package pro.dbro.ble.ui.activities;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import pro.dbro.ble.ChatClient;
import pro.dbro.ble.R;

/**
 * Created by davidbrodsky on 10/13/14.
 */
public class Util {

    public static void showWelcomeDialog(@NonNull final ChatClient app, @NonNull final Context context, DialogInterface.OnDismissListener dismissListener) {
        View dialogView = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
                            .inflate(R.layout.dialog_welcome, null);
        final EditText aliasEntry = ((EditText) dialogView.findViewById(R.id.aliasEntry));

        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        final AlertDialog dialog = builder.setTitle(context.getString(R.string.dialog_welcome_greeting))
                .setView(dialogView)
                .setPositiveButton(context.getString(R.string.dialog_ok), new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        app.createPrimaryIdentity(aliasEntry.getText().toString());
                    }
                })
                .setOnDismissListener(dismissListener)
                .show();
        aliasEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
                app.createPrimaryIdentity(textView.getText().toString());
                dialog.dismiss();
                return false;
            }
        });
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/adapter/CursorFilter.java
================================================
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pro.dbro.ble.ui.adapter;

import android.database.Cursor;
import android.widget.Filter;

/**
 * The CursorFilter delegates most of the work to the
 * {@link android.widget.CursorAdapter}. Subclasses should override these
 * delegate methods to run the queries and convert the results into String
 * that can be used by auto-completion widgets.
 */
class CursorFilter extends Filter {

    CursorFilterClient mClient;

    interface CursorFilterClient {
        CharSequence convertToString(Cursor cursor);
        Cursor runQueryOnBackgroundThread(CharSequence constraint);
        Cursor getCursor();
        void changeCursor(Cursor cursor);
    }

    CursorFilter(CursorFilterClient client) {
        mClient = client;
    }

    @Override
    public CharSequence convertResultToString(Object resultValue) {
        return mClient.convertToString((Cursor) resultValue);
    }

    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
        Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);

        FilterResults results = new FilterResults();
        if (cursor != null) {
            results.count = cursor.getCount();
            results.values = cursor;
        } else {
            results.count = 0;
            results.values = null;
        }
        return results;
    }

    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        Cursor oldCursor = mClient.getCursor();

        if (results.values != null && results.values != oldCursor) {
            mClient.changeCursor((Cursor) results.values);
        }
    }
}

================================================
FILE: app/src/main/java/pro/dbro/ble/ui/adapter/MessageAdapter.java
================================================
package pro.dbro.ble.ui.adapter;

import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.text.ParseException;
import java.util.UUID;

import im.delight.android.identicons.SymmetricIdenticon;
import pro.dbro.ble.R;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.DataUtil;
import pro.dbro.ble.data.model.MessageTable;
import pro.dbro.ble.data.model.Peer;

/**
 * Created by davidbrodsky on 10/19/14.
 */
public class MessageAdapter extends RecyclerViewCursorAdapter<MessageAdapter.ViewHolder> {
    public static final String TAG = "MessageAdapter";

    public interface MessageSelectedListener {
        void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId);
    }

    private DataStore mDataStore;
    private RecyclerView mHost;
    private MessageSelectedListener mListener;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public View container;
        public TextView senderView;
        public TextView messageView;
        public TextView authoredView;
        public SymmetricIdenticon identicon;
        public Peer peer;


        public ViewHolder(View v) {
            super(v);
            container = v;
            senderView = (TextView) v.findViewById(R.id.sender);
            messageView = (TextView) v.findViewById(R.id.messageBody);
            authoredView = (TextView) v.findViewById(R.id.authoredDate);
            identicon = (SymmetricIdenticon) v.findViewById(R.id.identicon);

        }
    }

    /**
     * Recommended constructor.
     *
     * @param context       The context
     * @param dataStore     The data backend
     * @param fromPeer      A Peer to show messages from, or null to show all messages
     * @param flags         Flags used to determine the behavior of the adapter;
     *                Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
     */
    public MessageAdapter(@NonNull Context context,
                          @Nullable Peer fromPeer,
                          @NonNull DataStore dataStore,
                          @Nullable MessageSelectedListener listener,
                          int flags) {
        super(context,
                fromPeer == null ? dataStore.getRecentMessages().getCursor() :
                                   dataStore.getRecentMessagesByPeer(fromPeer).getCursor(), flags);
        mDataStore = dataStore;
        mListener = listener;
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mHost = recyclerView;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
        holder.container.setTag(R.id.view_tag_msg_id, cursor.getInt(cursor.getColumnIndex(MessageTable.id)));

        if (holder.peer == null) // TODO : Should do this lookup on a background thread
            holder.peer = mDataStore.getPeerById(cursor.getInt(cursor.getColumnIndex(MessageTable.peerId)));

        if (holder.peer != null) {
            holder.container.setTag(R.id.view_tag_peer_id, holder.peer.getId());
            holder.senderView.setText(holder.peer.getAlias());
            holder.identicon.show(new String(holder.peer.getPublicKey()));
        } else {
            holder.senderView.setText("?");
            holder.identicon.show(UUID.randomUUID());
        }
        holder.messageView.setText(cursor.getString(cursor.getColumnIndex(MessageTable.body)));
        try {
            holder.authoredView.setText(DateUtils.getRelativeTimeSpanString(
                    DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(MessageTable.authoredDate))).getTime()));
        } catch (ParseException e) {
            holder.authoredView.setText("");
            e.printStackTrace();
        }
    }

    @Override
    protected void onContentChanged() {
        Log.i(TAG, "onContentChanged");
        changeCursor(mDataStore.getRecentMessages().getCursor());
        mHost.smoothScrollToPosition(0);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.message_item, parent, false);

        v.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null)
                    mListener.onMessageSelected(v.findViewById(R.id.identicon),
                                                v.findViewById(R.id.sender),
                                                (Integer) v.getTag(R.id.view_tag_msg_id),
                                                (Integer) v.getTag(R.id.view_tag_peer_id));
            }
        });
        // set the view's size, margins, paddings and layout parameters
        return new ViewHolder(v);
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/adapter/PeerAdapter.java
================================================
package pro.dbro.ble.ui.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;

import im.delight.android.identicons.SymmetricIdenticon;
import pro.dbro.ble.R;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.Message;
import pro.dbro.ble.data.model.Peer;

/**
 * Created by davidbrodsky on 10/12/14.
 */
public class PeerAdapter extends RecyclerView.Adapter<PeerAdapter.ViewHolder> {
    private Context mContext;
    private ArrayList<Peer> mPeers;

    // Provide a reference to the type of views that you are using
    // (custom viewholder)
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        SymmetricIdenticon mIdenticon;

        public ViewHolder(View v) {
            super(v);
            mTextView = (TextView) v.findViewById(R.id.username);
            mIdenticon = (SymmetricIdenticon) v.findViewById(R.id.identicon);
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public PeerAdapter(Context context, ArrayList<Peer> peers) {
        mPeers = peers;
        mContext = context;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public PeerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        // create a new view
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.peer_item, parent, false);
        // set the view's size, margins, paddings and layout parameters
        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        Peer peer = mPeers.get(position);
        holder.mTextView.setText(peer.getAlias());
        holder.mIdenticon.show(new String(peer.getPublicKey()));
    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return mPeers.size();
    }

    public void notifyPeerAdded(Peer peer) {
        mPeers.add(peer);
        notifyItemInserted(mPeers.size()-1);
    }

    public void notifyPeerRemoved(Peer peer) {
        int idx = mPeers.indexOf(peer);
        if (idx != -1) {
            mPeers.remove(idx);
            notifyItemRemoved(idx);
        }
    }

    public void clearPeers() {
        mPeers.clear();
        notifyDataSetChanged();
    }

    public void notifyMessageReceived(DataStore manager, Message message) {
        Peer peer = message.getSender(manager);
        if (peer != null) {
            int oldIdx = mPeers.indexOf(peer);
            if (oldIdx != -1 ) {
                mPeers.remove(peer);
                mPeers.add(0, peer);
                notifyItemMoved(oldIdx, 0);
            }
        }
    }
}

================================================
FILE: app/src/main/java/pro/dbro/ble/ui/adapter/RecyclerViewCursorAdapter.java
================================================
package pro.dbro.ble.ui.adapter;

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Copyright (C) 2014 flzyup@ligux.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.widget.Filter;
import android.widget.FilterQueryProvider;
import android.widget.Filterable;

/**
 * Version 1.0
 * <p/>
 * Date: 2014-07-07 19:53
 * Author: flzyup@ligux.com
 * <p/>
 * Copyright © 2009-2014 LiGux.com.
 */
public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> implements Filterable,
        CursorFilter.CursorFilterClient {

    /**
     * Call when bind view with the cursor
     *
     * @param holder
     * @param cursor
     */
    public abstract void onBindViewHolder(VH holder, Cursor cursor);

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected boolean mDataValid;

    /**
     * The current cursor
     */
    protected Cursor mCursor;

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected Context mContext;

    /**
     * The row id column
     */
    protected int mRowIDColumn;

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected ChangeObserver mChangeObserver;
    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected DataSetObserver mDataSetObserver;

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected CursorFilter mCursorFilter;

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected FilterQueryProvider mFilterQueryProvider;

    /**
     * If set the adapter will register a content observer on the cursor and will call
     * {@link #onContentChanged()} when a notification comes in.  Be careful when
     * using this flag: you will need to unset the current Cursor from the adapter
     * to avoid leaks due to its registered observers.  This flag is not needed
     * when using a CursorAdapter with a
     * {@link android.content.CursorLoader}.
     */
    public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;

    /**
     * Recommended constructor.
     *
     * @param c       The cursor from which to get the data.
     * @param context The context
     * @param flags   Flags used to determine the behavior of the adapter;
     *                Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
     */
    public RecyclerViewCursorAdapter(Context context, Cursor c, int flags) {
        init(context, c, flags);
    }

    void init(Context context, Cursor c, int flags) {

        boolean cursorPresent = c != null;
        mCursor = c;
        mDataValid = cursorPresent;
        mContext = context;
        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
        if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
            mChangeObserver = new ChangeObserver();
            mDataSetObserver = new MyDataSetObserver();
        } else {
            mChangeObserver = null;
            mDataSetObserver = null;
        }

        if (cursorPresent) {
            if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
            if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
        }
        setHasStableIds(true);
    }

    /**
     * Returns the cursor.
     *
     * @return the cursor.
     */
    @Override
    public Cursor getCursor() {
        return mCursor;
    }

    /**
     * @see android.support.v7.widget.RecyclerView.Adapter#getItemCount()
     */
    @Override
    public int getItemCount() {
        if (mDataValid && mCursor != null) {
            return mCursor.getCount();
        } else {
            return 0;
        }
    }

    /**
     * @param position Adapter position to query
     * @return
     * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)
     */
    @Override
    public long getItemId(int position) {
        if (mDataValid && mCursor != null) {
            if (mCursor.moveToPosition(position)) {
                return mCursor.getLong(mRowIDColumn);
            } else {
                return 0;
            }
        } else {
            return 0;
        }
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        if (!mDataValid) {
            throw new IllegalStateException("this should only be called when the cursor is valid");
        }
        if (!mCursor.moveToPosition(position)) {
            throw new IllegalStateException("couldn't move cursor to position " + position);
        }
        onBindViewHolder(holder, mCursor);
    }

    /**
     * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
     * closed.
     *
     * @param cursor The new cursor to be used
     */
    public void changeCursor(Cursor cursor) {
        Cursor old = swapCursor(cursor);
        if (old != null) {
            old.close();
        }
    }

    /**
     * Swap in a new Cursor, returning the old Cursor.  Unlike
     * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
     * closed.
     *
     * @param newCursor The new cursor to be used.
     * @return Returns the previously set Cursor, or null if there wasa not one.
     * If the given new Cursor is the same instance is the previously set
     * Cursor, null is also returned.
     */
    public Cursor swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return null;
        }
        Cursor oldCursor = mCursor;
        if (oldCursor != null) {
            if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
            if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
        }
        mCursor = newCursor;
        if (newCursor != null) {
            if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
            if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
            mDataValid = true;
            // notify the observers about the new cursor
            notifyDataSetChanged();
        } else {
            mRowIDColumn = -1;
            mDataValid = false;
            // notify the observers about the lack of a data set
            notifyDataSetChanged();
//            notifyDataSetInvalidated();
        }
        return oldCursor;
    }

    /**
     * <p>Converts the cursor into a CharSequence. Subclasses should override this
     * method to convert their results. The default implementation returns an
     * empty String for null values or the default String representation of
     * the value.</p>
     *
     * @param cursor the cursor to convert to a CharSequence
     * @return a CharSequence representing the value
     */
    public CharSequence convertToString(Cursor cursor) {
        return cursor == null ? "" : cursor.toString();
    }

    /**
     * Runs a query with the specified constraint. This query is requested
     * by the filter attached to this adapter.
     * <p/>
     * The query is provided by a
     * {@link android.widget.FilterQueryProvider}.
     * If no provider is specified, the current cursor is not filtered and returned.
     * <p/>
     * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
     * and the previous cursor is closed.
     * <p/>
     * This method is always executed on a background thread, not on the
     * application's main thread (or UI thread.)
     * <p/>
     * Contract: when constraint is null or empty, the original results,
     * prior to any filtering, must be returned.
     *
     * @param constraint the constraint with which the query must be filtered
     * @return a Cursor representing the results of the new query
     * @see #getFilter()
     * @see #getFilterQueryProvider()
     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
     */
    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
        if (mFilterQueryProvider != null) {
            return mFilterQueryProvider.runQuery(constraint);
        }

        return mCursor;
    }

    public Filter getFilter() {
        if (mCursorFilter == null) {
            mCursorFilter = new CursorFilter(this);
        }
        return mCursorFilter;
    }

    /**
     * Returns the query filter provider used for filtering. When the
     * provider is null, no filtering occurs.
     *
     * @return the current filter query provider or null if it does not exist
     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
     * @see #runQueryOnBackgroundThread(CharSequence)
     */
    public FilterQueryProvider getFilterQueryProvider() {
        return mFilterQueryProvider;
    }

    /**
     * Sets the query filter provider used to filter the current Cursor.
     * The provider's
     * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
     * method is invoked when filtering is requested by a client of
     * this adapter.
     *
     * @param filterQueryProvider the filter query provider or null to remove it
     * @see #getFilterQueryProvider()
     * @see #runQueryOnBackgroundThread(CharSequence)
     */
    public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
        mFilterQueryProvider = filterQueryProvider;
    }

    /**
     * Called when the {@link ContentObserver} on the cursor receives a change notification.
     * The default implementation provides the auto-requery logic, but may be overridden by
     * sub classes.
     *
     * @see ContentObserver#onChange(boolean)
     */
    protected abstract void onContentChanged();

    private class ChangeObserver extends ContentObserver {
        public ChangeObserver() {
            super(new Handler());
        }

        @Override
        public boolean deliverSelfNotifications() {
            return true;
        }

        @Override
        public void onChange(boolean selfChange) {
            onContentChanged();
        }
    }

    private class MyDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            mDataValid = true;
            notifyDataSetChanged();
        }

        @Override
        public void onInvalidated() {
            mDataValid = false;
            notifyDataSetChanged();
//            notifyDataSetInvalidated();
        }
    }
}

================================================
FILE: app/src/main/java/pro/dbro/ble/ui/adapter/StatusArrayAdapter.java
================================================
package pro.dbro.ble.ui.adapter;

import android.content.Context;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;

import pro.dbro.ble.R;
import timber.log.Timber;

/**
 * Created by davidbrodsky on 4/20/15.
 */
public class StatusArrayAdapter extends ArrayAdapter<String> {

    public StatusArrayAdapter(Context context, ArrayList<String> statuses) {
        super(context, android.R.layout.simple_spinner_dropdown_item, statuses);
    }

    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return getCustomView(position, convertView, parent);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return getCustomView(position, convertView, parent);
    }

    public View getCustomView(int position, View convertView, ViewGroup parent) {

        Context context = parent.getContext();

        // Get the data item for this position
        String status = getItem(position);
        // Check if an existing view is being reused, otherwise inflate the view
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_spinner_dropdown_item, parent, false);
            ((TextView) convertView).setCompoundDrawablePadding((int) dipToPixels(context, 8));
        }

        TextView statusLabel = (TextView) convertView;
        statusLabel.setText(status);

        String[] choices = context.getResources().getStringArray(R.array.status_options);
        if (status.equals(choices[0])) { // Always online
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_always_online), null, null, null);
        }
        else if (status.equals(choices[1])) { // Online when using app
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_online_in_foreground), null, null, null);
        } else if (status.equals(choices[2])) { // Offline
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_offline), null, null, null);
        } else {
            Timber.e("Unknown status. Cannot set adapter view correctly");
            statusLabel.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
        }

        return convertView;
    }

    public static float dipToPixels(Context context, float dipValue) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics);
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/fragment/MessagingFragment.java
================================================
package pro.dbro.ble.ui.fragment;


import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;

import pro.dbro.ble.R;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.ui.adapter.MessageAdapter;

/**
 * A Fragment that currently allows chatting only in the public broadcast mode
 * ala Twitter.
 */
public class MessagingFragment extends Fragment implements MessageAdapter.MessageSelectedListener {
    public static final String TAG = "MessageListFragment";

    public static interface ChatFragmentCallback {
        public void onMessageSendRequested(String message);
        public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId);
    }

    private ChatFragmentCallback mCallback;
    DataStore mDataStore;
    RecyclerView mRecyclerView;
    MessageAdapter mAdapter;
    EditText mMessageEntry;
    View mRoot;

    public MessagingFragment() {
        // Required empty public constructor
    }

    public void setDataStore(DataStore dataStore) {
        mDataStore = dataStore;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        if (mDataStore == null)
            throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore");

        // Inflate the layout for this fragment
        mRoot = inflater.inflate(R.layout.fragment_message, container, false);
        mMessageEntry = (EditText) mRoot.findViewById(R.id.messageEntry);
        mMessageEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEND) {
                    sendMessage(v.getText().toString());
                    v.setText("");
                    return true;
                }
                return false;
            }
        });
        mRoot.findViewById(R.id.sendMessageButton).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onSendMessageButtonClick(v);
            }
        });
        mRecyclerView = (RecyclerView) mRoot.findViewById(R.id.recyclerView);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mAdapter = new MessageAdapter(getActivity(), null, mDataStore, this, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        mRecyclerView.setAdapter(mAdapter);
        return mRoot;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mCallback = (ChatFragmentCallback) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement ChatFragmentCallback");
        }
    }

    public void onSendMessageButtonClick(View v) {
        sendMessage(mMessageEntry.getText().toString());
        mMessageEntry.setText("");
    }

    private void sendMessage(String message) {
        if (message.length() == 0) return;
        Log.i(TAG, "Sending message " + message);
        // For now treat all messsages as public broadcast
        mCallback.onMessageSendRequested(message);

    }

    @Override
    public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId) {
        mCallback.onMessageSelected(identiconView, usernameView, messageId, peerId);
    }

    public void animateIn() {
        mRoot.setAlpha(0);
        ObjectAnimator animator = ObjectAnimator.ofFloat(mRoot, "alpha", 0f, 1f)
                .setDuration(300);

        animator.setStartDelay(550);
        animator.start();
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/fragment/ProfileFragment.java
================================================
package pro.dbro.ble.ui.fragment;


import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import pro.dbro.ble.R;
import pro.dbro.ble.data.DataStore;
import pro.dbro.ble.data.model.Peer;
import pro.dbro.ble.ui.adapter.MessageAdapter;

/**
 * A Fragment that displays all messages from a particular peer
 */
public class ProfileFragment extends Fragment {

    DataStore mDataStore;
    RecyclerView mRecyclerView;
    MessageAdapter mAdapter;
    Peer mFromPeer;

//    TextView mUsernameView;

    public static ProfileFragment createForPeer(@NonNull DataStore dataStore,
                                                @NonNull Peer peer) {

        ProfileFragment frag = new ProfileFragment();
        frag.setFromPeer(peer);
        frag.setDataStore(dataStore);
        return frag;
    }

    public ProfileFragment() {
        // Required empty public constructor
    }

    public void setFromPeer(Peer fromPeer) {
        mFromPeer = fromPeer;
    }

    public void setDataStore(DataStore dataStore) {
        mDataStore = dataStore;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        if (mDataStore == null)
            throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore");

        // Inflate the layout for this fragment
        final View root = inflater.inflate(R.layout.fragment_peer_profile, container, false);
        mRecyclerView = (RecyclerView) root.findViewById(R.id.recyclerView);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mAdapter = new MessageAdapter(getActivity(), mFromPeer, mDataStore, null, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        mRecyclerView.setAdapter(mAdapter);

//        SymmetricIdenticon identicon = (SymmetricIdenticon) root.findViewById(R.id.profile_identicon);
//        ((SymmetricIdenticon) root.findViewById(R.id.profile_identicon)).show(new String(mFromPeer.getPublicKey()));
//        mUsernameView = ((TextView) root.findViewById(R.id.profile_name));
//        mUsernameView.setText(mFromPeer.getAlias());

        return root;
    }
}


================================================
FILE: app/src/main/java/pro/dbro/ble/ui/fragment/WelcomeFragment.java
================================================
package pro.dbro.ble.ui.fragment;


import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;

import pro.dbro.ble.R;

public class WelcomeFragment extends Fragment {

    public interface WelcomeFragmentCallback {
        public void onNameChosen(String name);
    }

    private WelcomeFragmentCallback mCallback;

    public WelcomeFragment() {
        // Required empty public constructor
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mCallback = (WelcomeFragmentCallback) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement WelcomeFragmentCallback");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_welcome, container, false);
        ((EditText) root.findViewById(R.id.aliasEntry)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
                mCallback.onNameChosen(textView.getText().toString());
                return false;
            }
        });
        return root;
    }


}


================================================
FILE: app/src/main/res/drawable/status_always_online.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid
        android:color="@color/status_always_online"/>

    <size
        android:width="20dp"
        android:height="20dp"/>
</shape>

================================================
FILE: app/src/main/res/drawable/status_offline.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid
        android:color="@color/status_offline"/>

    <size
        android:width="20dp"
        android:height="20dp"/>
</shape>

================================================
FILE: app/src/main/res/drawable/status_online_in_foreground.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid
        android:color="@color/status_online_in_foreground"/>

    <size
        android:width="20dp"
        android:height="20dp"/>
</shape>

================================================
FILE: app/src/main/res/drawable/transparent_button.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/primary">
    <item>
       
Download .txt
gitextract_ophh8epy/

├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE.txt
├── README.md
├── android-wait-for-emulator.sh
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── libs/
│   │   └── kalium-jni-1.0.2.jar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── pro/
│       │           └── dbro/
│       │               └── ble/
│       │                   ├── ChatAppTest.java
│       │                   └── util/
│       │                       └── RandomString.java
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   ├── com/
│           │   │   └── google/
│           │   │       └── samples/
│           │   │           └── apps/
│           │   │               └── iosched/
│           │   │                   └── ui/
│           │   │                       └── widget/
│           │   │                           └── ScrimInsetsScrollView.java
│           │   ├── im/
│           │   │   └── delight/
│           │   │       └── android/
│           │   │           └── identicons/
│           │   │               ├── AsymmetricIdenticon.java
│           │   │               ├── Identicon.java
│           │   │               └── SymmetricIdenticon.java
│           │   └── pro/
│           │       └── dbro/
│           │           └── ble/
│           │               ├── ActivityRecevingMessagesIndicator.java
│           │               ├── ChatApp.java
│           │               ├── ChatClient.java
│           │               ├── ChatPeerFlow.java
│           │               ├── PrefsManager.java
│           │               ├── crypto/
│           │               │   ├── KeyPair.java
│           │               │   └── SodiumShaker.java
│           │               ├── data/
│           │               │   ├── ContentProviderStore.java
│           │               │   ├── DataStore.java
│           │               │   └── model/
│           │               │       ├── ChatContentProvider.java
│           │               │       ├── ChatDatabase.java
│           │               │       ├── CursorModel.java
│           │               │       ├── DataUtil.java
│           │               │       ├── IdentityDeliveryTable.java
│           │               │       ├── Message.java
│           │               │       ├── MessageCollection.java
│           │               │       ├── MessageDeliveryTable.java
│           │               │       ├── MessageTable.java
│           │               │       ├── Peer.java
│           │               │       └── PeerTable.java
│           │               ├── protocol/
│           │               │   ├── BLEProtocol.java
│           │               │   ├── IdentityPacket.java
│           │               │   ├── MessagePacket.java
│           │               │   ├── NoDataPacket.java
│           │               │   ├── OwnedIdentityPacket.java
│           │               │   └── Protocol.java
│           │               └── ui/
│           │                   ├── Notification.java
│           │                   ├── activities/
│           │                   │   ├── LogConsumer.java
│           │                   │   ├── MainActivity.java
│           │                   │   └── Util.java
│           │                   ├── adapter/
│           │                   │   ├── CursorFilter.java
│           │                   │   ├── MessageAdapter.java
│           │                   │   ├── PeerAdapter.java
│           │                   │   ├── RecyclerViewCursorAdapter.java
│           │                   │   └── StatusArrayAdapter.java
│           │                   └── fragment/
│           │                       ├── MessagingFragment.java
│           │                       ├── ProfileFragment.java
│           │                       └── WelcomeFragment.java
│           └── res/
│               ├── drawable/
│               │   ├── status_always_online.xml
│               │   ├── status_offline.xml
│               │   ├── status_online_in_foreground.xml
│               │   └── transparent_button.xml
│               ├── layout/
│               │   ├── activity_main.xml
│               │   ├── dialog_welcome.xml
│               │   ├── fragment_message.xml
│               │   ├── fragment_peer.xml
│               │   ├── fragment_peer_profile.xml
│               │   ├── fragment_welcome.xml
│               │   ├── message_item.xml
│               │   ├── peer_item.xml
│               │   └── status_item.xml
│               ├── menu/
│               │   ├── menu_debug.xml
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── attrs.xml
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── ids.xml
│               │   ├── ints.xml
│               │   ├── strings-machine.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── pull_on_app_database.sh
└── settings.gradle
Download .txt
SYMBOL INDEX (352 symbols across 44 files)

FILE: app/src/androidTest/java/pro/dbro/ble/ChatAppTest.java
  class ChatAppTest (line 29) | public class ChatAppTest extends ApplicationTestCase<Application> {
    method ChatAppTest (line 30) | public ChatAppTest() {
    method setUp (line 40) | protected void setUp() throws Exception {
    method tearDown (line 50) | @Override
    method testCreateAndConsumeIdentityResponse (line 60) | public void testCreateAndConsumeIdentityResponse() {
    method testCreateAndConsumeMessageResponse (line 74) | public void testCreateAndConsumeMessageResponse() {
    method testApplicationIdentityCreationAndMessageConsumption (line 92) | public void testApplicationIdentityCreationAndMessageConsumption() thr...
    method testDatabaseQueryByBlob (line 139) | public void testDatabaseQueryByBlob() {
    method getOrCreatePrimaryPeerIdentity (line 187) | private Peer getOrCreatePrimaryPeerIdentity() throws IOException {
    method assertDateIsRecent (line 196) | private void assertDateIsRecent(Date mustBeRecent) {

FILE: app/src/androidTest/java/pro/dbro/ble/util/RandomString.java
  class RandomString (line 5) | public class RandomString {
    method RandomString (line 22) | public RandomString(int length) {
    method nextString (line 28) | public String nextString() {

FILE: app/src/main/java/com/google/samples/apps/iosched/ui/widget/ScrimInsetsScrollView.java
  class ScrimInsetsScrollView (line 34) | public class ScrimInsetsScrollView extends ScrollView {
    method ScrimInsetsScrollView (line 41) | public ScrimInsetsScrollView(Context context) {
    method ScrimInsetsScrollView (line 46) | public ScrimInsetsScrollView(Context context, AttributeSet attrs) {
    method ScrimInsetsScrollView (line 51) | public ScrimInsetsScrollView(Context context, AttributeSet attrs, int ...
    method init (line 56) | private void init(Context context, AttributeSet attrs, int defStyle) {
    method fitSystemWindows (line 68) | @Override
    method draw (line 79) | @Override
    method onAttachedToWindow (line 113) | @Override
    method onDetachedFromWindow (line 121) | @Override
    method setOnInsetsCallback (line 135) | public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) {
    type OnInsetsCallback (line 139) | public static interface OnInsetsCallback {
      method onInsetsChanged (line 140) | public void onInsetsChanged(Rect insets);

FILE: app/src/main/java/im/delight/android/identicons/AsymmetricIdenticon.java
  class AsymmetricIdenticon (line 23) | public class AsymmetricIdenticon extends Identicon {
    method AsymmetricIdenticon (line 25) | public AsymmetricIdenticon(Context context) {
    method AsymmetricIdenticon (line 29) | public AsymmetricIdenticon(Context context, AttributeSet attrs) {
    method AsymmetricIdenticon (line 33) | public AsymmetricIdenticon(Context context, AttributeSet attrs, int de...
    method isCellVisible (line 37) | @Override
    method getIconColor (line 42) | @Override
    method getRowCount (line 47) | @Override
    method getColumnCount (line 52) | @Override

FILE: app/src/main/java/im/delight/android/identicons/Identicon.java
  class Identicon (line 30) | abstract public class Identicon extends View {
    method Identicon (line 42) | public Identicon(Context context) {
    method Identicon (line 52) | public Identicon(Context context, AttributeSet attrs) {
    method Identicon (line 62) | public Identicon(Context context, AttributeSet attrs, int defStyleAttr) {
    method init (line 72) | @SuppressLint("NewApi")
    method show (line 84) | public void show(String input) {
    method show (line 111) | public void show(int input) {
    method show (line 115) | public void show(long input) {
    method show (line 119) | public void show(float input) {
    method show (line 123) | public void show(double input) {
    method show (line 127) | public void show(byte input) {
    method show (line 131) | public void show(char input) {
    method show (line 135) | public void show(boolean input) {
    method show (line 139) | public void show(Object input) {
    method setupColors (line 148) | protected void setupColors() {
    method getByte (line 164) | protected byte getByte(int index) {
    method getRowCount (line 173) | abstract protected int getRowCount();
    method getColumnCount (line 175) | abstract protected int getColumnCount();
    method isCellVisible (line 177) | abstract protected boolean isCellVisible(int row, int column);
    method getIconColor (line 179) | abstract protected int getIconColor();
    method onSizeChanged (line 181) | @Override
    method onMeasure (line 189) | @Override
    method onDraw (line 196) | @Override

FILE: app/src/main/java/im/delight/android/identicons/SymmetricIdenticon.java
  class SymmetricIdenticon (line 23) | public class SymmetricIdenticon extends Identicon {
    method SymmetricIdenticon (line 27) | public SymmetricIdenticon(Context context) {
    method SymmetricIdenticon (line 31) | public SymmetricIdenticon(Context context, AttributeSet attrs) {
    method SymmetricIdenticon (line 35) | public SymmetricIdenticon(Context context, AttributeSet attrs, int def...
    method getSymmetricColumnIndex (line 39) | protected int getSymmetricColumnIndex(int row) {
    method isCellVisible (line 48) | @Override
    method getIconColor (line 53) | @Override
    method getRowCount (line 58) | @Override
    method getColumnCount (line 63) | @Override

FILE: app/src/main/java/pro/dbro/ble/ActivityRecevingMessagesIndicator.java
  type ActivityRecevingMessagesIndicator (line 9) | public interface ActivityRecevingMessagesIndicator {
    method isActivityReceivingMessages (line 11) | public boolean isActivityReceivingMessages();

FILE: app/src/main/java/pro/dbro/ble/ChatApp.java
  class ChatApp (line 12) | public class ChatApp extends Application {
    method onCreate (line 14) | @Override public void onCreate() {

FILE: app/src/main/java/pro/dbro/ble/ChatClient.java
  class ChatClient (line 30) | public class ChatClient implements AirShareService.Callback,
    type Callback (line 34) | public interface Callback {
      method onAppPeerStatusUpdated (line 36) | void onAppPeerStatusUpdated(@NonNull Peer remotePeer,
    method ChatClient (line 56) | public ChatClient(@NonNull Context context) {
    method setAirShareServiceBinder (line 63) | public void setAirShareServiceBinder(AirShareService.ServiceBinder bin...
    method setCallback (line 68) | public void setCallback(Callback callback) {
    method makeAvailable (line 74) | public void makeAvailable() {
    method makeUnavailable (line 89) | public void makeUnavailable() {
    method getPrimaryLocalPeer (line 98) | public Peer getPrimaryLocalPeer() {
    method createPrimaryIdentity (line 102) | public Peer createPrimaryIdentity(String alias) {
    method sendPublicMessageFromPrimaryIdentity (line 111) | public void sendPublicMessageFromPrimaryIdentity(String body) {
    method getDataStore (line 132) | public DataStore getDataStore() {
    method onAppPeerStatusUpdated (line 140) | @Override
    method onMessageSent (line 163) | @Override
    method onMessageReceived (line 170) | @Override
    method onDataRecevied (line 184) | @Override
    method onDataSent (line 200) | @Override
    method onPeerStatusUpdated (line 216) | @Override
    method onPeerTransportUpdated (line 237) | @Override
    method sendData (line 245) | @Override

FILE: app/src/main/java/pro/dbro/ble/ChatPeerFlow.java
  class ChatPeerFlow (line 35) | public class ChatPeerFlow {
    class UnexpectedDataException (line 37) | public static class UnexpectedDataException extends Exception {
      method UnexpectedDataException (line 38) | public UnexpectedDataException(String detailMessage) {
    type DataOutlet (line 44) | public static interface DataOutlet {
      method sendData (line 45) | public void sendData(Peer peer, byte[] data);
    type Callback (line 48) | public static interface Callback {
      type ConnectionStatus (line 50) | public static enum ConnectionStatus { CONNECTED, DISCONNECTED }
      method onAppPeerStatusUpdated (line 52) | public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,
      method onMessageSent (line 56) | public void onMessageSent(@NonNull ChatPeerFlow flow,
      method onMessageReceived (line 60) | public void onMessageReceived(@NonNull ChatPeerFlow flow,
    type State (line 68) | public static enum State { CLIENT_WRITE_ID, HOST_WRITE_ID, CLIENT_WRIT...
    method ChatPeerFlow (line 87) | public ChatPeerFlow(DataStore dataStore,
    method isComplete (line 107) | public boolean isComplete() {
    method getRemoteAirSharePeer (line 111) | public Peer getRemoteAirSharePeer() {
    method queueMessage (line 115) | public void queueMessage(MessagePacket message) {
    method onDataSent (line 123) | public boolean onDataSent(byte[] data) throws UnexpectedDataException {
    method onDataReceived (line 209) | public boolean onDataReceived(byte[] data) throws UnexpectedDataExcept...
    method sendIdentity (line 298) | private void sendIdentity() {
    method sendMessage (line 314) | private void sendMessage() {
    method incrementStateAndSendAsAppropriate (line 327) | private void incrementStateAndSendAsAppropriate() {
    method sendAsAppropriate (line 339) | private void sendAsAppropriate() {
    method getMessagesForIdentity (line 365) | private ArrayDeque<MessagePacket> getMessagesForIdentity(@Nullable byt...
    method getIdentitiesForIdentity (line 400) | private ArrayDeque<IdentityPacket> getIdentitiesForIdentity(@Nullable ...

FILE: app/src/main/java/pro/dbro/ble/PrefsManager.java
  class PrefsManager (line 9) | public class PrefsManager {
    method getStatus (line 17) | public static int getStatus(Context context) {
    method setStatus (line 22) | public static void setStatus(Context context, int status) {
    method clearState (line 28) | public static void clearState(Context context) {

FILE: app/src/main/java/pro/dbro/ble/crypto/KeyPair.java
  class KeyPair (line 6) | public class KeyPair {
    method KeyPair (line 11) | public KeyPair(byte[] publicKey, byte[] secretKey) {

FILE: app/src/main/java/pro/dbro/ble/crypto/SodiumShaker.java
  class SodiumShaker (line 13) | public class SodiumShaker {
    method generateKeyPair (line 29) | public static KeyPair generateKeyPair() {
    method generateSignatureForMessage (line 37) | public static byte[] generateSignatureForMessage(@NonNull byte[] secre...
    method verifySignature (line 54) | public static boolean verifySignature(@NonNull byte[] public_key, @Non...

FILE: app/src/main/java/pro/dbro/ble/data/ContentProviderStore.java
  class ContentProviderStore (line 39) | public class ContentProviderStore extends DataStore {
    method ContentProviderStore (line 42) | public ContentProviderStore(Context context) {
    method markMessageDeliveredToPeer (line 46) | @Override
    method markIdentityDeliveredToPeer (line 65) | @Override
    method createLocalPeerWithAlias (line 83) | @Nullable
    method getPrimaryLocalPeer (line 105) | @Override
    method getOutgoingMessagesForPeer (line 122) | @Nullable
    method getOutgoingIdentitiesForPeer (line 143) | @Override
    method getRecentMessages (line 164) | @Override
    method getRecentMessagesByPeer (line 178) | @Override
    method createOrUpdateRemotePeerWithProtocolIdentity (line 192) | @Nullable
    method createOrUpdateMessageWithProtocolMessage (line 233) | @Nullable
    method getMessageBySignature (line 271) | @Nullable
    method getMessageById (line 286) | @Nullable
    method getPeerByPubKey (line 299) | @Nullable
    method getPeerById (line 316) | @Nullable
    method countPeers (line 333) | @Override
    method countMessagesPassed (line 349) | @Override
    method haveDeliveredMessageToPeer (line 367) | private boolean haveDeliveredMessageToPeer(Message message, Peer peer) {
    method haveDeliveredPeerIdentityToPeer (line 383) | private boolean haveDeliveredPeerIdentityToPeer(Peer peerPayload, Peer...

FILE: app/src/main/java/pro/dbro/ble/data/DataStore.java
  class DataStore (line 22) | public abstract class DataStore {
    method DataStore (line 26) | public DataStore(@NonNull Context context) {
    method markMessageDeliveredToPeer (line 30) | public abstract void markMessageDeliveredToPeer(@NonNull MessagePacket...
    method markIdentityDeliveredToPeer (line 32) | public abstract void markIdentityDeliveredToPeer(@NonNull IdentityPack...
    method createLocalPeerWithAlias (line 34) | public abstract Peer createLocalPeerWithAlias(@NonNull String alias, @...
    method getPrimaryLocalPeer (line 36) | public abstract Peer getPrimaryLocalPeer();
    method getOutgoingMessagesForPeer (line 38) | public abstract List<MessagePacket> getOutgoingMessagesForPeer(@NonNul...
    method getOutgoingIdentitiesForPeer (line 40) | public abstract List<IdentityPacket> getOutgoingIdentitiesForPeer(@Non...
    method getRecentMessages (line 42) | public abstract MessageCollection getRecentMessages();
    method getRecentMessagesByPeer (line 44) | public abstract MessageCollection getRecentMessagesByPeer(@NonNull Pee...
    method createOrUpdateRemotePeerWithProtocolIdentity (line 46) | public abstract Peer createOrUpdateRemotePeerWithProtocolIdentity(@Non...
    method createOrUpdateMessageWithProtocolMessage (line 48) | public abstract Message createOrUpdateMessageWithProtocolMessage(@NonN...
    method getMessageBySignature (line 50) | public abstract Message getMessageBySignature(@NonNull byte[] signature);
    method getMessageById (line 52) | public abstract Message getMessageById(int id);
    method getPeerByPubKey (line 54) | public abstract Peer getPeerByPubKey(@NonNull byte[] publicKey);
    method getPeerById (line 56) | public abstract Peer getPeerById(int id);
    method countPeers (line 58) | public abstract int countPeers();
    method countMessagesPassed (line 60) | public abstract int countMessagesPassed();

FILE: app/src/main/java/pro/dbro/ble/data/model/ChatContentProvider.java
  class ChatContentProvider (line 15) | @ContentProvider(authority = ChatContentProvider.AUTHORITY, database = C...
    method buildUri (line 21) | private static Uri buildUri(String... paths) {
    class Peers (line 31) | @TableEndpoint(table = ChatDatabase.PEERS)
    class Messages (line 45) | @TableEndpoint(table = ChatDatabase.MESSAGES)
    class MessageDeliveries (line 60) | @TableEndpoint(table = ChatDatabase.DELIVERED_MESSAGES)
    class IdentityDeliveries (line 75) | @TableEndpoint(table = ChatDatabase.DELIVERED_IDENTITIES)

FILE: app/src/main/java/pro/dbro/ble/data/model/ChatDatabase.java
  class ChatDatabase (line 11) | @Database(version = ChatDatabase.DATABASE_VERSION)

FILE: app/src/main/java/pro/dbro/ble/data/model/CursorModel.java
  class CursorModel (line 11) | public abstract class CursorModel implements Closeable{
    method CursorModel (line 19) | public CursorModel(@NonNull Cursor cursor) {
    method getCursor (line 23) | public Cursor getCursor() {
    method close (line 27) | @Override

FILE: app/src/main/java/pro/dbro/ble/data/model/DataUtil.java
  class DataUtil (line 11) | public class DataUtil {
    method bytesToHex (line 23) | public static String bytesToHex(byte[] bytes) {

FILE: app/src/main/java/pro/dbro/ble/data/model/IdentityDeliveryTable.java
  type IdentityDeliveryTable (line 15) | public interface IdentityDeliveryTable {

FILE: app/src/main/java/pro/dbro/ble/data/model/Message.java
  class Message (line 21) | public class Message extends CursorModel {
    method Message (line 23) | public Message(@NonNull Cursor cursor) {
    method getId (line 28) | public int getId() {
    method getBody (line 32) | public String getBody() {
    method getAuthoredDate (line 36) | public Date getAuthoredDate() {
    method getPublicKey (line 45) | public byte[] getPublicKey(DataStore dataStore) {
    method getSignature (line 49) | public byte[] getSignature() {
    method getReplySignature (line 53) | public byte[] getReplySignature() {
    method getRawPacket (line 57) | public byte[] getRawPacket() {
    method getSender (line 61) | @Nullable
    method getProtocolMessage (line 66) | @Nullable
    method getRelativeReceivedDate (line 78) | @Nullable

FILE: app/src/main/java/pro/dbro/ble/data/model/MessageCollection.java
  class MessageCollection (line 11) | public class MessageCollection extends CursorModel {
    method MessageCollection (line 13) | public MessageCollection(@NonNull Cursor cursor) {
    method getMessageAtPosition (line 17) | @Nullable
    method getCursor (line 25) | public Cursor getCursor() {

FILE: app/src/main/java/pro/dbro/ble/data/model/MessageDeliveryTable.java
  type MessageDeliveryTable (line 15) | public interface MessageDeliveryTable {

FILE: app/src/main/java/pro/dbro/ble/data/model/MessageTable.java
  type MessageTable (line 15) | public interface MessageTable {

FILE: app/src/main/java/pro/dbro/ble/data/model/Peer.java
  class Peer (line 16) | public class Peer {
    method Peer (line 27) | public Peer(@NonNull Cursor cursor) {
    method getId (line 41) | public int getId() {
    method getPublicKey (line 45) | public byte[] getPublicKey() {
    method getAlias (line 49) | public String getAlias() {
    method getLastDateSeen (line 53) | @Nullable
    method isLocalPeer (line 61) | public boolean isLocalPeer() {
    method getIdentity (line 71) | public IdentityPacket getIdentity() {
    method equals (line 79) | @Override

FILE: app/src/main/java/pro/dbro/ble/data/model/PeerTable.java
  type PeerTable (line 15) | public interface PeerTable {

FILE: app/src/main/java/pro/dbro/ble/protocol/BLEProtocol.java
  class BLEProtocol (line 17) | public class BLEProtocol implements Protocol {
    method serializeIdentity (line 46) | @Nullable
    method serializeMessage (line 71) | @Nullable
    method serializeNoDataPacket (line 97) | @NonNull
    method deserializeIdentity (line 118) | @Nullable
    method deserializeMessageWithIdentity (line 153) | @Nullable
    method deserializeMessage (line 168) | @Nullable
    method deserializeNoDataPacket (line 205) | @NonNull
    method getPacketType (line 233) | public byte getPacketType(@NonNull byte[] message) {
    method addVersionToBuffer (line 243) | private static int addVersionToBuffer(@NonNull byte[] input, int offse...
    method getVersionFromBuffer (line 251) | private static int getVersionFromBuffer(@NonNull byte[] input, @NonNul...
    method addTypeToBuffer (line 258) | private static int addTypeToBuffer(@NonNull byte[] input, byte type, i...
    method getTypeFromBuffer (line 266) | private static int getTypeFromBuffer(@NonNull byte[] input, @NonNull b...
    method addTimestampToBuffer (line 273) | private static int addTimestampToBuffer(@NonNull byte[] input, int off...
    method addPublicKeyToBuffer (line 286) | private static int addPublicKeyToBuffer(@NonNull byte[] public_key, @N...
    method addAliasToBuffer (line 294) | private static int addAliasToBuffer(@NonNull String alias, @NonNull by...
    method addMessageBodyToBuffer (line 307) | private static int addMessageBodyToBuffer(@NonNull String body, @NonNu...
    method getBytesFromBuffer (line 320) | private static int getBytesFromBuffer(@NonNull byte[] input, @NonNull ...
    method addSignatureToBuffer (line 331) | private static int addSignatureToBuffer(@NonNull byte[] secret_key, @N...
    method truncateOrPadTextBuffer (line 347) | private static void truncateOrPadTextBuffer(byte[] input, byte[] outpu...
    method assertBufferLength (line 356) | private static void assertBufferLength(byte[] input, int minimumLength) {
    method assertBufferVersion (line 361) | private static int assertBufferVersion(byte[] input, int offset) {
    method assertBufferType (line 370) | private static int assertBufferType(byte[] input, byte expectedType, i...
    method getDateFromTimestampBuffer (line 379) | @Nullable

FILE: app/src/main/java/pro/dbro/ble/protocol/IdentityPacket.java
  class IdentityPacket (line 12) | public class IdentityPacket {
    method IdentityPacket (line 20) | public IdentityPacket(@NonNull final byte[] publicKey, @Nullable Strin...

FILE: app/src/main/java/pro/dbro/ble/protocol/MessagePacket.java
  class MessagePacket (line 10) | public class MessagePacket {
    method MessagePacket (line 21) | public MessagePacket(@NonNull final byte[] publicKey,
    method attachIdentityToMessage (line 36) | public static MessagePacket attachIdentityToMessage(@NonNull MessagePa...
    method MessagePacket (line 41) | public MessagePacket(@NonNull IdentityPacket sender,

FILE: app/src/main/java/pro/dbro/ble/protocol/NoDataPacket.java
  class NoDataPacket (line 11) | public class NoDataPacket {
    method NoDataPacket (line 19) | public NoDataPacket(@NonNull final byte[] publicKey,

FILE: app/src/main/java/pro/dbro/ble/protocol/OwnedIdentityPacket.java
  class OwnedIdentityPacket (line 9) | public class OwnedIdentityPacket extends IdentityPacket {
    method OwnedIdentityPacket (line 13) | public OwnedIdentityPacket(@NonNull final byte[] secretKey, @NonNull f...

FILE: app/src/main/java/pro/dbro/ble/protocol/Protocol.java
  type Protocol (line 9) | public interface Protocol {
    method serializeIdentity (line 17) | public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIden...
    method serializeMessage (line 19) | public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket own...
    method serializeNoDataPacket (line 21) | public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket...
    method deserializeIdentity (line 28) | public IdentityPacket deserializeIdentity(@NonNull byte[] identity);
    method deserializeMessageWithIdentity (line 31) | public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] me...
    method deserializeMessage (line 34) | public MessagePacket deserializeMessage(@NonNull byte[] message);
    method getPacketType (line 36) | public byte getPacketType(@NonNull byte[] message);

FILE: app/src/main/java/pro/dbro/ble/ui/Notification.java
  class Notification (line 27) | public class Notification {
    method displayPeerAvailableNotification (line 45) | public static void displayPeerAvailableNotification(@NonNull Context c...
    method displayMessageNotification (line 74) | public static void displayMessageNotification(@NonNull Context context...
    method makePendingIntent (line 120) | private static PendingIntent makePendingIntent(@NonNull Context contex...
    method loadBitmapFromView (line 130) | public static Bitmap loadBitmapFromView(View v, int width, int height) {

FILE: app/src/main/java/pro/dbro/ble/ui/activities/LogConsumer.java
  type LogConsumer (line 6) | public interface LogConsumer {
    method onLogEvent (line 7) | public void onLogEvent(String event);

FILE: app/src/main/java/pro/dbro/ble/ui/activities/MainActivity.java
  class MainActivity (line 58) | public class MainActivity extends AppCompatActivity implements LogConsumer,
    method onCreate (line 100) | @Override
    method revealChatViews (line 219) | private void revealChatViews() {
    method refreshProfileStats (line 231) | private void refreshProfileStats() {
    method onLogEvent (line 240) | @Override
    method checkUserRegistered (line 259) | private void checkUserRegistered() {
    method onServiceReady (line 284) | @Override
    method onFinished (line 297) | @Override
    method onMessageSendRequested (line 302) | @Override
    method onMessageSelected (line 307) | @Override
    method onAppPeerStatusUpdated (line 373) | @Override
    method tintSystemBars (line 393) | private void tintSystemBars(final int toolbarFromColor, final int stat...
    method onBackPressed (line 417) | @Override
    method onNameChosen (line 426) | @Override

FILE: app/src/main/java/pro/dbro/ble/ui/activities/Util.java
  class Util (line 19) | public class Util {
    method showWelcomeDialog (line 21) | public static void showWelcomeDialog(@NonNull final ChatClient app, @N...

FILE: app/src/main/java/pro/dbro/ble/ui/adapter/CursorFilter.java
  class CursorFilter (line 28) | class CursorFilter extends Filter {
    type CursorFilterClient (line 32) | interface CursorFilterClient {
      method convertToString (line 33) | CharSequence convertToString(Cursor cursor);
      method runQueryOnBackgroundThread (line 34) | Cursor runQueryOnBackgroundThread(CharSequence constraint);
      method getCursor (line 35) | Cursor getCursor();
      method changeCursor (line 36) | void changeCursor(Cursor cursor);
    method CursorFilter (line 39) | CursorFilter(CursorFilterClient client) {
    method convertResultToString (line 43) | @Override
    method performFiltering (line 48) | @Override
    method publishResults (line 63) | @Override

FILE: app/src/main/java/pro/dbro/ble/ui/adapter/MessageAdapter.java
  class MessageAdapter (line 28) | public class MessageAdapter extends RecyclerViewCursorAdapter<MessageAda...
    type MessageSelectedListener (line 31) | public interface MessageSelectedListener {
      method onMessageSelected (line 32) | void onMessageSelected(View identiconView, View usernameView, int me...
    class ViewHolder (line 39) | public static class ViewHolder extends RecyclerView.ViewHolder {
      method ViewHolder (line 48) | public ViewHolder(View v) {
    method MessageAdapter (line 68) | public MessageAdapter(@NonNull Context context,
    method onAttachedToRecyclerView (line 80) | @Override
    method onBindViewHolder (line 85) | @Override
    method onContentChanged (line 110) | @Override
    method onCreateViewHolder (line 117) | @Override

FILE: app/src/main/java/pro/dbro/ble/ui/adapter/PeerAdapter.java
  class PeerAdapter (line 21) | public class PeerAdapter extends RecyclerView.Adapter<PeerAdapter.ViewHo...
    class ViewHolder (line 27) | public static class ViewHolder extends RecyclerView.ViewHolder {
      method ViewHolder (line 31) | public ViewHolder(View v) {
    method PeerAdapter (line 39) | public PeerAdapter(Context context, ArrayList<Peer> peers) {
    method onCreateViewHolder (line 45) | @Override
    method onBindViewHolder (line 57) | @Override
    method getItemCount (line 67) | @Override
    method notifyPeerAdded (line 72) | public void notifyPeerAdded(Peer peer) {
    method notifyPeerRemoved (line 77) | public void notifyPeerRemoved(Peer peer) {
    method clearPeers (line 85) | public void clearPeers() {
    method notifyMessageReceived (line 90) | public void notifyMessageReceived(DataStore manager, Message message) {

FILE: app/src/main/java/pro/dbro/ble/ui/adapter/RecyclerViewCursorAdapter.java
  class RecyclerViewCursorAdapter (line 53) | public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView....
    method onBindViewHolder (line 62) | public abstract void onBindViewHolder(VH holder, Cursor cursor);
    method RecyclerViewCursorAdapter (line 127) | public RecyclerViewCursorAdapter(Context context, Cursor c, int flags) {
    method init (line 131) | void init(Context context, Cursor c, int flags) {
    method getCursor (line 158) | @Override
    method getItemCount (line 166) | @Override
    method getItemId (line 180) | @Override
    method onBindViewHolder (line 193) | @Override
    method changeCursor (line 210) | public void changeCursor(Cursor cursor) {
    method swapCursor (line 227) | public Cursor swapCursor(Cursor newCursor) {
    method convertToString (line 263) | public CharSequence convertToString(Cursor cursor) {
    method runQueryOnBackgroundThread (line 290) | public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
    method getFilter (line 298) | public Filter getFilter() {
    method getFilterQueryProvider (line 313) | public FilterQueryProvider getFilterQueryProvider() {
    method setFilterQueryProvider (line 328) | public void setFilterQueryProvider(FilterQueryProvider filterQueryProv...
    method onContentChanged (line 339) | protected abstract void onContentChanged();
    class ChangeObserver (line 341) | private class ChangeObserver extends ContentObserver {
      method ChangeObserver (line 342) | public ChangeObserver() {
      method deliverSelfNotifications (line 346) | @Override
      method onChange (line 351) | @Override
    class MyDataSetObserver (line 357) | private class MyDataSetObserver extends DataSetObserver {
      method onChanged (line 358) | @Override
      method onInvalidated (line 364) | @Override

FILE: app/src/main/java/pro/dbro/ble/ui/adapter/StatusArrayAdapter.java
  class StatusArrayAdapter (line 20) | public class StatusArrayAdapter extends ArrayAdapter<String> {
    method StatusArrayAdapter (line 22) | public StatusArrayAdapter(Context context, ArrayList<String> statuses) {
    method getDropDownView (line 26) | @Override
    method getView (line 31) | @Override
    method getCustomView (line 36) | public View getCustomView(int position, View convertView, ViewGroup pa...
    method dipToPixels (line 67) | public static float dipToPixels(Context context, float dipValue) {

FILE: app/src/main/java/pro/dbro/ble/ui/fragment/MessagingFragment.java
  class MessagingFragment (line 27) | public class MessagingFragment extends Fragment implements MessageAdapte...
    type ChatFragmentCallback (line 30) | public static interface ChatFragmentCallback {
      method onMessageSendRequested (line 31) | public void onMessageSendRequested(String message);
      method onMessageSelected (line 32) | public void onMessageSelected(View identiconView, View usernameView,...
    method MessagingFragment (line 42) | public MessagingFragment() {
    method setDataStore (line 46) | public void setDataStore(DataStore dataStore) {
    method onCreateView (line 50) | @Override
    method onAttach (line 84) | @Override
    method onSendMessageButtonClick (line 95) | public void onSendMessageButtonClick(View v) {
    method sendMessage (line 100) | private void sendMessage(String message) {
    method onMessageSelected (line 108) | @Override
    method animateIn (line 113) | public void animateIn() {

FILE: app/src/main/java/pro/dbro/ble/ui/fragment/ProfileFragment.java
  class ProfileFragment (line 21) | public class ProfileFragment extends Fragment {
    method createForPeer (line 30) | public static ProfileFragment createForPeer(@NonNull DataStore dataStore,
    method ProfileFragment (line 39) | public ProfileFragment() {
    method setFromPeer (line 43) | public void setFromPeer(Peer fromPeer) {
    method setDataStore (line 47) | public void setDataStore(DataStore dataStore) {
    method onCreateView (line 51) | @Override

FILE: app/src/main/java/pro/dbro/ble/ui/fragment/WelcomeFragment.java
  class WelcomeFragment (line 16) | public class WelcomeFragment extends Fragment {
    type WelcomeFragmentCallback (line 18) | public interface WelcomeFragmentCallback {
      method onNameChosen (line 19) | public void onNameChosen(String name);
    method WelcomeFragment (line 24) | public WelcomeFragment() {
    method onAttach (line 28) | @Override
    method onCreate (line 38) | @Override
    method onCreateView (line 43) | @Override
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (239K chars).
[
  {
    "path": ".gitignore",
    "chars": 67,
    "preview": ".crashes\n.gradle\n/local.properties\n/.idea/\n.DS_Store\n/build\n*.iml\n\n"
  },
  {
    "path": ".gitmodules",
    "chars": 123,
    "preview": "[submodule \"submodules/airshare\"]\n\tpath = submodules/airshare\n\turl = https://github.com/OnlyInAmerica/AirShare-Android.g"
  },
  {
    "path": ".travis.yml",
    "chars": 604,
    "preview": "language: android\nandroid:\n  update-sdk: true\n  components:\n    - platform-tools\n    - build-tools-22.0.1\n    - android-"
  },
  {
    "path": "LICENSE.txt",
    "chars": 15922,
    "preview": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means each individual or legal entity that"
  },
  {
    "path": "README.md",
    "chars": 1522,
    "preview": "# [BLEMeshChat Android](https://github.com/OnlyInAmerica/BLEMeshChat) [![Build Status](https://travis-ci.org/OnlyInAmeri"
  },
  {
    "path": "android-wait-for-emulator.sh",
    "chars": 646,
    "preview": "#!/bin/bash\n\n# Originally written by Ralf Kistner <ralf@embarkmobile.com>, but placed in the public domain\n\nset +e\n\nboot"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 1577,
    "preview": "apply plugin: 'com.android.application'\napply plugin: 'android-apt'\napply plugin: 'com.jakewharton.hugo'\n\nandroid {\n    "
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 667,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /A"
  },
  {
    "path": "app/src/androidTest/java/pro/dbro/ble/ChatAppTest.java",
    "chars": 8189,
    "preview": "package pro.dbro.ble;\n\nimport android.app.Application;\nimport android.content.ContentValues;\nimport android.database.Cur"
  },
  {
    "path": "app/src/androidTest/java/pro/dbro/ble/util/RandomString.java",
    "chars": 829,
    "preview": "package pro.dbro.ble.util;\n\nimport java.util.Random;\n\npublic class RandomString {\n\n    private static final char[] symbo"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1050,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package="
  },
  {
    "path": "app/src/main/java/com/google/samples/apps/iosched/ui/widget/ScrimInsetsScrollView.java",
    "chars": 4672,
    "preview": "/*\n * Copyright 2014 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not us"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/AsymmetricIdenticon.java",
    "chars": 1474,
    "preview": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\n * \n * Licensed under the"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/Identicon.java",
    "chars": 5006,
    "preview": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\n * \n * Licensed under the"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/SymmetricIdenticon.java",
    "chars": 1711,
    "preview": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\n * \n * Licensed under the"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ActivityRecevingMessagesIndicator.java",
    "chars": 352,
    "preview": "package pro.dbro.ble;\n\n/**\n * Implemented by a Service or other entity to report an Activity is bound, and thus\n * in th"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatApp.java",
    "chars": 909,
    "preview": "package pro.dbro.ble;\n\nimport android.app.Application;\n\nimport com.facebook.stetho.Stetho;\n\nimport timber.log.Timber;\n\n/"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatClient.java",
    "chars": 9539,
    "preview": "package pro.dbro.ble;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\nimport android.support"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatPeerFlow.java",
    "chars": 16837,
    "preview": "package pro.dbro.ble;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport ja"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/PrefsManager.java",
    "chars": 871,
    "preview": "package pro.dbro.ble;\n\nimport android.content.Context;\n\n\n/**\n * Created by davidbrodsky on 9/21/14.\n */\npublic class Pre"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/crypto/KeyPair.java",
    "chars": 310,
    "preview": "package pro.dbro.ble.crypto;\n\n/**\n * Created by davidbrodsky on 10/22/14.\n */\npublic class KeyPair {\n\n    public final b"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/crypto/SodiumShaker.java",
    "chars": 2109,
    "preview": "package pro.dbro.ble.crypto;\n\nimport android.support.annotation.NonNull;\n\nimport org.abstractj.kalium.NaCl;\nimport org.a"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/ContentProviderStore.java",
    "chars": 15786,
    "preview": "package pro.dbro.ble.data;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.databas"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/DataStore.java",
    "chars": 2098,
    "preview": "package pro.dbro.ble.data;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\nimport android.su"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/ChatContentProvider.java",
    "chars": 2665,
    "preview": "package pro.dbro.ble.data.model;\n\nimport android.net.Uri;\n\nimport net.simonvt.schematic.annotation.ContentProvider;\nimpo"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/ChatDatabase.java",
    "chars": 853,
    "preview": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.Database;\nimport net.simonvt.schematic.annotat"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/CursorModel.java",
    "chars": 735,
    "preview": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\n\nimport jav"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/DataUtil.java",
    "chars": 1053,
    "preview": "package pro.dbro.ble.data.model;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\n\n/**\n * Utilities for conv"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/IdentityDeliveryTable.java",
    "chars": 852,
    "preview": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.an"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/Message.java",
    "chars": 2472,
    "preview": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport andr"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageCollection.java",
    "chars": 616,
    "preview": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport andr"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageDeliveryTable.java",
    "chars": 849,
    "preview": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.an"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageTable.java",
    "chars": 1323,
    "preview": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.an"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/Peer.java",
    "chars": 2490,
    "preview": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport andr"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/PeerTable.java",
    "chars": 1139,
    "preview": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.an"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/BLEProtocol.java",
    "chars": 17530,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\ni"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/IdentityPacket.java",
    "chars": 864,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/MessagePacket.java",
    "chars": 1941,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\nimport java.util.Date;\n\n/**\n * Created by da"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/NoDataPacket.java",
    "chars": 745,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\nimport java.util.Date;\n\n/**\n * Created by da"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/OwnedIdentityPacket.java",
    "chars": 518,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\n/**\n * An Identity for the local peer\n * Cre"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/Protocol.java",
    "chars": 1166,
    "preview": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/Notification.java",
    "chars": 6087,
    "preview": "package pro.dbro.ble.ui;\n\nimport android.app.NotificationManager;\nimport android.app.PendingIntent;\nimport android.app.T"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/LogConsumer.java",
    "chars": 160,
    "preview": "package pro.dbro.ble.ui.activities;\n\n/**\n * Created by davidbrodsky on 10/11/14.\n */\npublic interface LogConsumer {\n    "
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/MainActivity.java",
    "chars": 17074,
    "preview": "package pro.dbro.ble.ui.activities;\n\nimport android.animation.ValueAnimator;\nimport android.app.Activity;\nimport android"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/Util.java",
    "chars": 1900,
    "preview": "package pro.dbro.ble.ui.activities;\n\nimport android.app.AlertDialog;\nimport android.content.Context;\nimport android.cont"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/CursorFilter.java",
    "chars": 2252,
    "preview": "/*\n * Copyright (C) 2011 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/MessageAdapter.java",
    "chars": 5182,
    "preview": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.support"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/PeerAdapter.java",
    "chars": 3196,
    "preview": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.support.v7.widget.RecyclerView;\nimport "
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/RecyclerViewCursorAdapter.java",
    "chars": 12049,
    "preview": "package pro.dbro.ble.ui.adapter;\n\n/*\n * Copyright (C) 2013 The Android Open Source Project\n *\n * Licensed under the Apac"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/StatusArrayAdapter.java",
    "chars": 2816,
    "preview": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.util.DisplayMetrics;\nimport android.uti"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/MessagingFragment.java",
    "chars": 4243,
    "preview": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.animation.ObjectAnimator;\nimport android.app.Activity;\nimport android"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/ProfileFragment.java",
    "chars": 2472,
    "preview": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.os.Bundle;\nimport android.support.annotation.NonNull;\nimport android."
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/WelcomeFragment.java",
    "chars": 1697,
    "preview": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.support.v4.app"
  },
  {
    "path": "app/src/main/res/drawable/status_always_online.xml",
    "chars": 281,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android"
  },
  {
    "path": "app/src/main/res/drawable/status_offline.xml",
    "chars": 275,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android"
  },
  {
    "path": "app/src/main/res/drawable/status_online_in_foreground.xml",
    "chars": 288,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android"
  },
  {
    "path": "app/src/main/res/drawable/transparent_button.xml",
    "chars": 301,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:co"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 7536,
    "preview": "<android.support.v4.widget.DrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http:/"
  },
  {
    "path": "app/src/main/res/layout/dialog_welcome.xml",
    "chars": 792,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    an"
  },
  {
    "path": "app/src/main/res/layout/fragment_message.xml",
    "chars": 1889,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/t"
  },
  {
    "path": "app/src/main/res/layout/fragment_peer.xml",
    "chars": 459,
    "preview": "<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tool"
  },
  {
    "path": "app/src/main/res/layout/fragment_peer_profile.xml",
    "chars": 465,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/t"
  },
  {
    "path": "app/src/main/res/layout/fragment_welcome.xml",
    "chars": 3329,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    androi"
  },
  {
    "path": "app/src/main/res/layout/message_item.xml",
    "chars": 2722,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.v7.widget.CardView xmlns:android=\"http://schemas.android.com/apk"
  },
  {
    "path": "app/src/main/res/layout/peer_item.xml",
    "chars": 771,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andr"
  },
  {
    "path": "app/src/main/res/layout/status_item.xml",
    "chars": 287,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:"
  },
  {
    "path": "app/src/main/res/menu/menu_debug.xml",
    "chars": 321,
    "preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    "
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "chars": 363,
    "preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    "
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "chars": 200,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"ScrimInsetsView\">\n        <attr name=\"in"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 663,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"primary_subdued\">#09307adf</color>\n    <color name=\""
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 211,
    "preview": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal"
  },
  {
    "path": "app/src/main/res/values/ids.xml",
    "chars": 154,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item type=\"id\" name=\"view_tag_msg_id\"/>\n    <item type=\"id\" name"
  },
  {
    "path": "app/src/main/res/values/ints.xml",
    "chars": 162,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <integer name=\"max_alias_length\">35</integer>\n    <integer name=\""
  },
  {
    "path": "app/src/main/res/values/strings-machine.xml",
    "chars": 192,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"identicon_transition_name\">identicon</string>\n    "
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 1872,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"app_name\">Sneakernet</string>\n    <string name=\"ac"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 698,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\""
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "chars": 358,
    "preview": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as s"
  },
  {
    "path": "build.gradle",
    "chars": 562,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    r"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 232,
    "preview": "#Wed Dec 10 14:12:56 PST 2014\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
  },
  {
    "path": "gradle.properties",
    "chars": 853,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Settings specified in this file will override any "
  },
  {
    "path": "gradlew",
    "chars": 5080,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2404,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "pull_on_app_database.sh",
    "chars": 251,
    "preview": "#!/bin/bash\n\n# Pull the database for this application to the Desktop\n\nadb backup -f ~/Desktop/ble.ab -noapk pro.dbro.ble"
  },
  {
    "path": "settings.gradle",
    "chars": 50,
    "preview": "include ':app'\ninclude ':submodules:airshare:sdk'\n"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the OnlyInAmerica/BLEMeshChat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (218.6 KB), approximately 51.2k tokens, and a symbol index with 352 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!