├── .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-xxhdpi │ ├── ic_drawer.png │ ├── ic_user.png │ └── ic_user_white_small.png │ ├── drawable │ ├── sneakernet.png │ ├── 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 │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ids.xml │ ├── ints.xml │ ├── strings-machine.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pull_on_app_database.sh └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .crashes 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | *.iml 8 | 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/airshare"] 2 | path = submodules/airshare 3 | url = https://github.com/OnlyInAmerica/AirShare-Android.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | update-sdk: true 4 | components: 5 | - platform-tools 6 | - build-tools-22.0.1 7 | - android-22 8 | - extra-android-m2repository 9 | - sys-img-armeabi-v7a-android-22 10 | 11 | #before_script: 12 | # - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a 13 | # - emulator -avd test -no-skin -no-audio -no-window & 14 | # - bash ./android-wait-for-emulator.sh 15 | # - adb shell input keyevent 82 & 16 | 17 | script: 18 | - ./gradlew assembleDebug 19 | # Currently only builds, does not run tests. 20 | # Enable testing with './gradlew connectedCheck' 21 | # once I figure out this emulator business 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [BLEMeshChat Android](https://github.com/OnlyInAmerica/BLEMeshChat) [![Build Status](https://travis-ci.org/OnlyInAmerica/BLEMeshChat.svg?branch=master)](https://travis-ci.org/OnlyInAmerica/BLEMeshChat) 2 | 3 | [![Screenshot](http://i.imgur.com/GMtn5ol.png)](http://i.imgur.com/GMtn5ol.png) 4 | 5 | **Under Development : Not yet ready for use!** 6 | 7 | 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. 8 | 9 | Also see the [iOS client](https://github.com/chrisballinger/BLEMeshChat) and [Protocol Spec](https://github.com/chrisballinger/BLEMeshChat/wiki). 10 | 11 | ## Motivation 12 | 13 | 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. 14 | 15 | Imagine: 16 | 17 | Broadcasting the locations of potable water in a disaster scenario without Internet. 18 | 19 | Seeking insulin at a crowded festival where cell service is unreliable. 20 | 21 | Organizing movements of a large protest where cellular Internet is jammed. 22 | 23 | ## Goals 24 | 25 | + This system must be able to operate constantly in the background without a significant effect on battery life. 26 | + Connections to peers must be made without user intervention. 27 | + Messages must be signed, and the system must allow the user to verify other users' association with a particular public key. 28 | 29 | ## License 30 | 31 | MPL 2.0 -------------------------------------------------------------------------------- /android-wait-for-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Originally written by Ralf Kistner , but placed in the public domain 4 | 5 | set +e 6 | 7 | bootanim="" 8 | failcounter=0 9 | timeout_in_sec=360 10 | 11 | until [[ "$bootanim" =~ "stopped" ]]; do 12 | bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &` 13 | if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" 14 | || "$bootanim" =~ "running" ]]; then 15 | let "failcounter += 1" 16 | echo "Waiting for emulator to start" 17 | if [[ $failcounter -gt timeout_in_sec ]]; then 18 | echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" 19 | exit 1 20 | fi 21 | fi 22 | sleep 1 23 | done 24 | 25 | echo "Emulator is ready" 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'android-apt' 3 | apply plugin: 'com.jakewharton.hugo' 4 | 5 | android { 6 | compileSdkVersion 22 7 | buildToolsVersion "22.0.1" 8 | 9 | defaultConfig { 10 | applicationId "pro.dbro.ble" 11 | minSdkVersion 21 12 | targetSdkVersion 22 13 | 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_7 27 | targetCompatibility JavaVersion.VERSION_1_7 28 | } 29 | 30 | packagingOptions { 31 | exclude 'META-INF/LICENSE.txt' 32 | exclude 'META-INF/NOTICE.txt' 33 | } 34 | } 35 | 36 | dependencies { 37 | compile fileTree(dir: 'libs', include: ['*.jar']) 38 | compile 'com.android.support:appcompat-v7:22.1.1' 39 | compile project(':submodules:airshare:sdk') 40 | apt 'net.simonvt.schematic:schematic-compiler:0.5.1' 41 | compile 'com.jakewharton.timber:timber:2.7.1' 42 | compile 'com.google.guava:guava:18.0' 43 | compile 'net.simonvt.schematic:schematic:0.5.3' 44 | compile 'com.android.support:support-annotations:20.0.0' 45 | compile 'com.jakewharton:butterknife:5.1.2' 46 | compile 'com.android.support:recyclerview-v7:21.0.3' 47 | compile 'com.android.support:cardview-v7:21.0.0' 48 | compile 'com.android.support:palette-v7:21.0.0' 49 | compile 'com.nispok:snackbar:2.10.6' 50 | compile 'com.facebook.stetho:stetho:1.1.1' 51 | } 52 | 53 | apt { 54 | arguments { 55 | schematicOutPackage 'pro.dbro.ble.schematic' 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/libs/kalium-jni-1.0.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnlyInAmerica/BLEMeshChat/d6dbf0c3c6ed0ebc9c9567c64468e3e1597e1414/app/libs/kalium-jni-1.0.2.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/pro/dbro/ble/ChatAppTest.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | import android.app.Application; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.test.ApplicationTestCase; 8 | 9 | import java.io.IOException; 10 | import java.util.Arrays; 11 | import java.util.Date; 12 | 13 | import pro.dbro.ble.crypto.KeyPair; 14 | import pro.dbro.ble.crypto.SodiumShaker; 15 | import pro.dbro.ble.data.ContentProviderStore; 16 | import pro.dbro.ble.data.model.ChatContentProvider; 17 | import pro.dbro.ble.data.model.DataUtil; 18 | import pro.dbro.ble.data.model.Peer; 19 | import pro.dbro.ble.data.model.PeerTable; 20 | import pro.dbro.ble.protocol.BLEProtocol; 21 | import pro.dbro.ble.protocol.IdentityPacket; 22 | import pro.dbro.ble.protocol.MessagePacket; 23 | import pro.dbro.ble.protocol.OwnedIdentityPacket; 24 | import pro.dbro.ble.util.RandomString; 25 | 26 | /** 27 | * Tests of the ChatProtocol and Chat Application. 28 | */ 29 | public class ChatAppTest extends ApplicationTestCase { 30 | public ChatAppTest() { 31 | super(Application.class); 32 | } 33 | 34 | ChatClient mApp; 35 | OwnedIdentityPacket mSenderIdentity; 36 | boolean mCreatedNewPrimaryIdentity; 37 | BLEProtocol bleProtocol = new BLEProtocol(); 38 | ContentProviderStore dataStore; 39 | 40 | protected void setUp() throws Exception { 41 | super.setUp(); 42 | 43 | mApp = new ChatClient(getContext()); 44 | dataStore = new ContentProviderStore(getContext()); 45 | String username = new RandomString(BLEProtocol.ALIAS_LENGTH).nextString(); 46 | KeyPair keyPair = SodiumShaker.generateKeyPair(); 47 | mSenderIdentity = new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, username, null); 48 | } 49 | 50 | @Override 51 | protected void tearDown() throws Exception { 52 | super.tearDown(); 53 | } 54 | 55 | /** Protocol Tests **/ 56 | 57 | /** 58 | * {@link pro.dbro.ble.protocol.IdentityPacket} -> byte[] -> {@link pro.dbro.ble.protocol.IdentityPacket} 59 | */ 60 | public void testCreateAndConsumeIdentityResponse() { 61 | byte[] identityResponse = bleProtocol.serializeIdentity(mSenderIdentity); 62 | 63 | // Parse Identity from sender's identityResponse response byte[] 64 | IdentityPacket parsedIdentityPacket = bleProtocol.deserializeIdentity(identityResponse); 65 | 66 | assertEquals(parsedIdentityPacket.alias, mSenderIdentity.alias); 67 | assertEquals(Arrays.equals(parsedIdentityPacket.publicKey, mSenderIdentity.publicKey), true); 68 | assertDateIsRecent(parsedIdentityPacket.dateSeen); 69 | } 70 | 71 | /** 72 | * {@link pro.dbro.ble.protocol.MessagePacket} -> byte[] -> {@link pro.dbro.ble.protocol.MessagePacket} 73 | */ 74 | public void testCreateAndConsumeMessageResponse() { 75 | String messageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString(); 76 | 77 | MessagePacket messageResponse = bleProtocol.serializeMessage(mSenderIdentity, messageBody); 78 | 79 | MessagePacket parsedMessagePacket = bleProtocol.deserializeMessage(messageResponse.rawPacket); 80 | 81 | assertEquals(messageBody, parsedMessagePacket.body); 82 | assertEquals(Arrays.equals(parsedMessagePacket.sender.publicKey, mSenderIdentity.publicKey), true); 83 | assertDateIsRecent(parsedMessagePacket.authoredDate); 84 | } 85 | 86 | /** Application Tests **/ 87 | 88 | /** 89 | * Create a {@link pro.dbro.ble.data.model.Peer} for protocol {@link pro.dbro.ble.protocol.IdentityPacket}, 90 | * then create a {@link pro.dbro.ble.data.model.Message} for protocol {@link pro.dbro.ble.protocol.MessagePacket}. 91 | */ 92 | public void testApplicationIdentityCreationAndMessageConsumption() throws IOException { 93 | // TODO : Rewrite for new API 94 | // Get or create new primary identity. This Identity serves as the app user 95 | Peer user = getOrCreatePrimaryPeerIdentity(); 96 | 97 | // User discovers a peer 98 | 99 | IdentityPacket remotePeer = bleProtocol.deserializeIdentity(bleProtocol.serializeIdentity(mSenderIdentity)); 100 | // Assert Identity response parsed successfully 101 | assertEquals(Arrays.equals(remotePeer.publicKey, mSenderIdentity.publicKey), true); 102 | 103 | // Craft a mock message from remote peer 104 | String mockReceivedMessageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString(); 105 | MessagePacket mockReceivedMessage = bleProtocol.serializeMessage(mSenderIdentity, mockReceivedMessageBody); 106 | 107 | // User receives mock message from remote peer 108 | // pro.dbro.ble.data.model.Message parsedMockReceivedMessage = mApp.consumeReceivedBroadcastMessage(getContext(), mockReceivedMessage); 109 | // assertEquals(mockReceivedMessageBody.equals(parsedMockReceivedMessage.getBody()), true); 110 | 111 | // Cleanup 112 | // TODO: Should mock database 113 | int numDeleted = 0; 114 | if (mCreatedNewPrimaryIdentity) { 115 | numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS, 116 | PeerTable.id + " = ?", 117 | new String[]{String.valueOf(user.getId())}); 118 | assertEquals(numDeleted, 1); 119 | numDeleted = 0; 120 | } 121 | 122 | // numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS, 123 | // PeerTable.id + " = ?", 124 | // new String[] {String.valueOf(remotePeer.getId())}); 125 | // 126 | // assertEquals(numDeleted, 1); 127 | // numDeleted = 0; 128 | // 129 | // numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Messages.MESSAGES, 130 | // MessageTable.id + " = ?", 131 | // new String[] {String.valueOf(parsedMockReceivedMessage.getId())}); 132 | // assertEquals(numDeleted, 1); 133 | // numDeleted = 0; 134 | } 135 | 136 | /** 137 | * Test database lookups by BLOB column 138 | */ 139 | public void testDatabaseQueryByBlob() { 140 | byte[] fakePubKey = new byte[] { (byte) 0x01 }; 141 | ContentValues stubPeer = new ContentValues(); 142 | stubPeer.put(PeerTable.alias, "test"); 143 | stubPeer.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date())); 144 | stubPeer.put(PeerTable.pubKey, fakePubKey); 145 | Uri stubPeerUri = getContext().getContentResolver().insert(ChatContentProvider.Peers.PEERS, stubPeer); 146 | 147 | int stubPeerId = Integer.parseInt(stubPeerUri.getLastPathSegment()); 148 | 149 | Cursor result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS, 150 | null, 151 | PeerTable.id + " = ?", 152 | new String[] { 153 | String.valueOf(stubPeerId) 154 | }, 155 | null); 156 | 157 | assertEquals(result != null, true); 158 | assertEquals(result.moveToFirst(), true); 159 | 160 | byte[] resultBlob = result.getBlob(result.getColumnIndex(PeerTable.pubKey)); 161 | 162 | assertEquals(Arrays.equals(resultBlob, fakePubKey), true); 163 | result.close(); 164 | 165 | result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS, 166 | null, 167 | "quote(" + PeerTable.pubKey + ") = ?", 168 | new String[] { 169 | "X'01'" 170 | }, 171 | null); 172 | 173 | assertEquals(result != null, true); 174 | assertEquals(result.moveToFirst(), true); 175 | 176 | // Cleanup 177 | 178 | int numDeleted = getContext().getContentResolver().delete((ChatContentProvider.Peers.PEERS), 179 | PeerTable.id + " = ?", 180 | new String[] { 181 | String.valueOf(stubPeerId) 182 | }); 183 | assertEquals(numDeleted ,1); 184 | } 185 | /** Utility **/ 186 | 187 | private Peer getOrCreatePrimaryPeerIdentity() throws IOException { 188 | Peer user = mApp.getPrimaryLocalPeer(); 189 | if (user == null) { 190 | mCreatedNewPrimaryIdentity = true; 191 | user = mApp.createPrimaryIdentity(new RandomString(BLEProtocol.ALIAS_LENGTH).nextString()); 192 | } 193 | return user; 194 | } 195 | 196 | private void assertDateIsRecent(Date mustBeRecent) { 197 | long now = new Date().getTime(); 198 | long oneSecondAgo = now - 1000; 199 | 200 | if ( (mustBeRecent.getTime() > now) ){ 201 | throw new IllegalStateException("Parsed Identity time is from the future " + mustBeRecent); 202 | 203 | } else if (mustBeRecent.getTime() < oneSecondAgo) { 204 | throw new IllegalStateException("Parsed Identity time is from more than 500ms ago " + mustBeRecent); 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/pro/dbro/ble/util/RandomString.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.util; 2 | 3 | import java.util.Random; 4 | 5 | public class RandomString { 6 | 7 | private static final char[] symbols; 8 | 9 | static { 10 | StringBuilder tmp = new StringBuilder(); 11 | for (char ch = '0'; ch <= '9'; ++ch) 12 | tmp.append(ch); 13 | for (char ch = 'a'; ch <= 'z'; ++ch) 14 | tmp.append(ch); 15 | symbols = tmp.toString().toCharArray(); 16 | } 17 | 18 | private final Random random = new Random(); 19 | 20 | private final char[] buf; 21 | 22 | public RandomString(int length) { 23 | if (length < 1) 24 | throw new IllegalArgumentException("length < 1: " + length); 25 | buf = new char[length]; 26 | } 27 | 28 | public String nextString() { 29 | for (int idx = 0; idx < buf.length; ++idx) 30 | buf[idx] = symbols[random.nextInt(symbols.length)]; 31 | return new String(buf); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/iosched/ui/widget/ScrimInsetsScrollView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.iosched.ui.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Rect; 23 | import android.graphics.drawable.Drawable; 24 | import android.support.v4.view.ViewCompat; 25 | import android.util.AttributeSet; 26 | import android.widget.ScrollView; 27 | 28 | import pro.dbro.ble.R; 29 | 30 | /** 31 | * A layout that draws something in the insets passed to {@link #fitSystemWindows(Rect)}, i.e. the area above UI chrome 32 | * (status and navigation bars, overlay action bars). 33 | */ 34 | public class ScrimInsetsScrollView extends ScrollView { 35 | private Drawable mInsetForeground; 36 | 37 | private Rect mInsets; 38 | private Rect mTempRect = new Rect(); 39 | private OnInsetsCallback mOnInsetsCallback; 40 | 41 | public ScrimInsetsScrollView(Context context) { 42 | super(context); 43 | init(context, null, 0); 44 | } 45 | 46 | public ScrimInsetsScrollView(Context context, AttributeSet attrs) { 47 | super(context, attrs); 48 | init(context, attrs, 0); 49 | } 50 | 51 | public ScrimInsetsScrollView(Context context, AttributeSet attrs, int defStyle) { 52 | super(context, attrs, defStyle); 53 | init(context, attrs, defStyle); 54 | } 55 | 56 | private void init(Context context, AttributeSet attrs, int defStyle) { 57 | final TypedArray a = context.obtainStyledAttributes(attrs, 58 | R.styleable.ScrimInsetsView, defStyle, 0); 59 | if (a == null) { 60 | return; 61 | } 62 | mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsView_insetForeground); 63 | a.recycle(); 64 | 65 | setWillNotDraw(true); 66 | } 67 | 68 | @Override 69 | protected boolean fitSystemWindows(Rect insets) { 70 | mInsets = new Rect(insets); 71 | setWillNotDraw(mInsetForeground == null); 72 | ViewCompat.postInvalidateOnAnimation(this); 73 | if (mOnInsetsCallback != null) { 74 | mOnInsetsCallback.onInsetsChanged(insets); 75 | } 76 | return true; // consume insets 77 | } 78 | 79 | @Override 80 | public void draw(Canvas canvas) { 81 | super.draw(canvas); 82 | 83 | int width = getWidth(); 84 | int height = getHeight(); 85 | if (mInsets != null && mInsetForeground != null) { 86 | int sc = canvas.save(); 87 | canvas.translate(getScrollX(), getScrollY()); 88 | 89 | // Top 90 | mTempRect.set(0, 0, width, mInsets.top); 91 | mInsetForeground.setBounds(mTempRect); 92 | mInsetForeground.draw(canvas); 93 | 94 | // Bottom 95 | mTempRect.set(0, height - mInsets.bottom, width, height); 96 | mInsetForeground.setBounds(mTempRect); 97 | mInsetForeground.draw(canvas); 98 | 99 | // Left 100 | mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom); 101 | mInsetForeground.setBounds(mTempRect); 102 | mInsetForeground.draw(canvas); 103 | 104 | // Right 105 | mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom); 106 | mInsetForeground.setBounds(mTempRect); 107 | mInsetForeground.draw(canvas); 108 | 109 | canvas.restoreToCount(sc); 110 | } 111 | } 112 | 113 | @Override 114 | protected void onAttachedToWindow() { 115 | super.onAttachedToWindow(); 116 | if (mInsetForeground != null) { 117 | mInsetForeground.setCallback(this); 118 | } 119 | } 120 | 121 | @Override 122 | protected void onDetachedFromWindow() { 123 | super.onDetachedFromWindow(); 124 | if (mInsetForeground != null) { 125 | mInsetForeground.setCallback(null); 126 | } 127 | } 128 | 129 | /** 130 | * Allows the calling container to specify a callback for custom processing when insets change (i.e. when 131 | * {@link #fitSystemWindows(Rect)} is called. This is useful for setting padding on UI elements based on 132 | * UI chrome insets (e.g. a Google Map or a ListView). When using with ListView or GridView, remember to set 133 | * clipToPadding to false. 134 | */ 135 | public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) { 136 | mOnInsetsCallback = onInsetsCallback; 137 | } 138 | 139 | public static interface OnInsetsCallback { 140 | public void onInsetsChanged(Rect insets); 141 | } 142 | } -------------------------------------------------------------------------------- /app/src/main/java/im/delight/android/identicons/AsymmetricIdenticon.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.identicons; 2 | 3 | /** 4 | * Copyright 2014 www.delight.im 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.graphics.Color; 21 | import android.util.AttributeSet; 22 | 23 | public class AsymmetricIdenticon extends Identicon { 24 | 25 | public AsymmetricIdenticon(Context context) { 26 | super(context); 27 | } 28 | 29 | public AsymmetricIdenticon(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | } 32 | 33 | public AsymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) { 34 | super(context, attrs, defStyleAttr); 35 | } 36 | 37 | @Override 38 | protected boolean isCellVisible(int row, int column) { 39 | return getByte(3 + row * getColumnCount() + column) >= 0; 40 | } 41 | 42 | @Override 43 | protected int getIconColor() { 44 | return Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128); 45 | } 46 | 47 | @Override 48 | protected int getRowCount() { 49 | return 4; 50 | } 51 | 52 | @Override 53 | protected int getColumnCount() { 54 | return 4; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/im/delight/android/identicons/Identicon.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.identicons; 2 | 3 | /** 4 | * Copyright 2014 www.delight.im 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.annotation.SuppressLint; 20 | import android.content.Context; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.Paint; 24 | import android.os.Build; 25 | import android.util.AttributeSet; 26 | import android.view.View; 27 | 28 | import java.security.MessageDigest; 29 | 30 | abstract public class Identicon extends View { 31 | 32 | private static final String HASH_ALGORITHM = "SHA-256"; 33 | private final int mRowCount; 34 | private final int mColumnCount; 35 | private final Paint mPaint; 36 | private volatile int mCellWidth; 37 | private volatile int mCellHeight; 38 | private volatile byte[] mHash; 39 | private volatile int[][] mColors; 40 | private volatile boolean mReady; 41 | 42 | public Identicon(Context context) { 43 | super(context); 44 | 45 | mRowCount = getRowCount(); 46 | mColumnCount = getColumnCount(); 47 | mPaint = new Paint(); 48 | 49 | init(); 50 | } 51 | 52 | public Identicon(Context context, AttributeSet attrs) { 53 | super(context, attrs); 54 | 55 | mRowCount = getRowCount(); 56 | mColumnCount = getColumnCount(); 57 | mPaint = new Paint(); 58 | 59 | init(); 60 | } 61 | 62 | public Identicon(Context context, AttributeSet attrs, int defStyleAttr) { 63 | super(context, attrs, defStyleAttr); 64 | 65 | mRowCount = getRowCount(); 66 | mColumnCount = getColumnCount(); 67 | mPaint = new Paint(); 68 | 69 | init(); 70 | } 71 | 72 | @SuppressLint("NewApi") 73 | protected void init() { 74 | mPaint.setStyle(Paint.Style.FILL); 75 | mPaint.setAntiAlias(true); 76 | mPaint.setDither(true); 77 | 78 | setWillNotDraw(false); 79 | if (Build.VERSION.SDK_INT >= 11) { 80 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 81 | } 82 | } 83 | 84 | public void show(String input) { 85 | // if the input was null 86 | if (input == null) { 87 | // we can't create a hash value and have nothing to show (draw to the view) 88 | mHash = null; 89 | } 90 | // if the input was a proper string (non-null) 91 | else { 92 | // generate a hash from the string to get unique but deterministic byte values 93 | try { 94 | final MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); 95 | digest.update(input == null ? new byte[0] : input.getBytes()); 96 | mHash = digest.digest(); 97 | } 98 | catch (Exception e) { 99 | mHash = null; 100 | } 101 | } 102 | 103 | // set up the cell colors according to the input that was provided via show(...) 104 | setupColors(); 105 | 106 | // this view may now be drawn (and thus must be re-drawn) 107 | mReady = true; 108 | invalidate(); 109 | } 110 | 111 | public void show(int input) { 112 | show(String.valueOf(input)); 113 | } 114 | 115 | public void show(long input) { 116 | show(String.valueOf(input)); 117 | } 118 | 119 | public void show(float input) { 120 | show(String.valueOf(input)); 121 | } 122 | 123 | public void show(double input) { 124 | show(String.valueOf(input)); 125 | } 126 | 127 | public void show(byte input) { 128 | show(String.valueOf(input)); 129 | } 130 | 131 | public void show(char input) { 132 | show(String.valueOf(input)); 133 | } 134 | 135 | public void show(boolean input) { 136 | show(String.valueOf(input)); 137 | } 138 | 139 | public void show(Object input) { 140 | if (input == null) { 141 | mHash = null; 142 | } 143 | else { 144 | show(String.valueOf(input)); 145 | } 146 | } 147 | 148 | protected void setupColors() { 149 | mColors = new int[mRowCount][mColumnCount]; 150 | int colorVisible = getIconColor(); 151 | 152 | for (int r = 0; r < mRowCount; r++) { 153 | for (int c = 0; c < mColumnCount; c++) { 154 | if (isCellVisible(r, c)) { 155 | mColors[r][c] = colorVisible; 156 | } 157 | else { 158 | mColors[r][c] = Color.TRANSPARENT; 159 | } 160 | } 161 | } 162 | } 163 | 164 | protected byte getByte(int index) { 165 | if (mHash == null) { 166 | return -128; 167 | } 168 | else { 169 | return mHash[index % mHash.length]; 170 | } 171 | } 172 | 173 | abstract protected int getRowCount(); 174 | 175 | abstract protected int getColumnCount(); 176 | 177 | abstract protected boolean isCellVisible(int row, int column); 178 | 179 | abstract protected int getIconColor(); 180 | 181 | @Override 182 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 183 | super.onSizeChanged(w, h, oldw, oldh); 184 | 185 | mCellWidth = w / mColumnCount; 186 | mCellHeight = h / mRowCount; 187 | } 188 | 189 | @Override 190 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 191 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 192 | int size = Math.min(getMeasuredWidth(), getMeasuredHeight()); 193 | setMeasuredDimension(size, size); 194 | } 195 | 196 | @Override 197 | protected void onDraw(Canvas canvas) { 198 | super.onDraw(canvas); 199 | if (mReady) { 200 | int x, y; 201 | for (int r = 0; r < mRowCount; r++) { 202 | for (int c = 0; c < mColumnCount; c++) { 203 | x = mCellWidth * c; 204 | y = mCellHeight * r; 205 | 206 | mPaint.setColor(mColors[r][c]); 207 | 208 | canvas.drawRect(x, y + mCellHeight, x + mCellWidth, y, mPaint); 209 | } 210 | } 211 | } 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /app/src/main/java/im/delight/android/identicons/SymmetricIdenticon.java: -------------------------------------------------------------------------------- 1 | package im.delight.android.identicons; 2 | 3 | /** 4 | * Copyright 2014 www.delight.im 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.graphics.Color; 21 | import android.util.AttributeSet; 22 | 23 | public class SymmetricIdenticon extends Identicon { 24 | 25 | private static final int CENTER_COLUMN_INDEX = 3; 26 | 27 | public SymmetricIdenticon(Context context) { 28 | super(context); 29 | } 30 | 31 | public SymmetricIdenticon(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | } 34 | 35 | public SymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) { 36 | super(context, attrs, defStyleAttr); 37 | } 38 | 39 | protected int getSymmetricColumnIndex(int row) { 40 | if (row < CENTER_COLUMN_INDEX) { 41 | return row; 42 | } 43 | else { 44 | return getColumnCount() - row - 1; 45 | } 46 | } 47 | 48 | @Override 49 | protected boolean isCellVisible(int row, int column) { 50 | return getByte(3 + row * CENTER_COLUMN_INDEX + getSymmetricColumnIndex(column)) >= 0; 51 | } 52 | 53 | @Override 54 | protected int getIconColor() { 55 | return Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128); 56 | } 57 | 58 | @Override 59 | protected int getRowCount() { 60 | return 5; 61 | } 62 | 63 | @Override 64 | protected int getColumnCount() { 65 | return 5; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ActivityRecevingMessagesIndicator.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | /** 4 | * Implemented by a Service or other entity to report an Activity is bound, and thus 5 | * in the foreground. e.g: Useful to determine whether to post message notifications. 6 | * 7 | * Created by davidbrodsky on 11/14/14. 8 | */ 9 | public interface ActivityRecevingMessagesIndicator { 10 | 11 | public boolean isActivityReceivingMessages(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ChatApp.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.stetho.Stetho; 6 | 7 | import timber.log.Timber; 8 | 9 | /** 10 | * Created by davidbrodsky on 4/17/15. 11 | */ 12 | public class ChatApp extends Application { 13 | 14 | @Override public void onCreate() { 15 | super.onCreate(); 16 | 17 | if (BuildConfig.DEBUG) { 18 | Timber.plant(new Timber.DebugTree()); 19 | 20 | Stetho.initialize( 21 | Stetho.newInitializerBuilder(this) 22 | .enableDumpapp( 23 | Stetho.defaultDumperPluginsProvider(this)) 24 | .enableWebKitInspector( 25 | Stetho.defaultInspectorModulesProvider(this)) 26 | .build()); 27 | } 28 | 29 | // If we abandon Timber logging in this app, enable below line 30 | // to enable Timber logging in sdk 31 | //Logging.forceLogging(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ChatClient.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.google.common.collect.BiMap; 8 | import com.google.common.collect.HashBiMap; 9 | 10 | import java.util.HashMap; 11 | 12 | import pro.dbro.airshare.app.AirShareService; 13 | import pro.dbro.airshare.transport.Transport; 14 | import pro.dbro.ble.data.ContentProviderStore; 15 | import pro.dbro.ble.data.DataStore; 16 | import pro.dbro.ble.data.model.DataUtil; 17 | import pro.dbro.ble.data.model.Message; 18 | import pro.dbro.ble.data.model.Peer; 19 | import pro.dbro.ble.protocol.BLEProtocol; 20 | import pro.dbro.ble.protocol.MessagePacket; 21 | import pro.dbro.ble.protocol.OwnedIdentityPacket; 22 | import pro.dbro.ble.protocol.Protocol; 23 | import pro.dbro.ble.ui.Notification; 24 | import pro.dbro.ble.ui.activities.LogConsumer; 25 | import timber.log.Timber; 26 | 27 | /** 28 | * Created by davidbrodsky on 10/13/14. 29 | */ 30 | public class ChatClient implements AirShareService.Callback, 31 | ChatPeerFlow.DataOutlet, 32 | ChatPeerFlow.Callback { 33 | 34 | public interface Callback { 35 | /** Client should not invoke remotePeer#close() */ 36 | void onAppPeerStatusUpdated(@NonNull Peer remotePeer, 37 | @NonNull ConnectionStatus status); 38 | } 39 | 40 | public static final String TAG = "ChatApp"; 41 | public static final String AIRSHARE_SERVICE_NAME = "BLEMeshChat"; 42 | 43 | private Context mContext; 44 | private DataStore mDataStore; 45 | private Protocol mProtocol; 46 | private AirShareService.ServiceBinder mAirShareServiceBinder; 47 | private Callback mCallback; 48 | 49 | private HashMap mFlows = new HashMap<>(); 50 | 51 | /** AirShare Peer -> BLEMeshChat Peer id */ 52 | private BiMap mConnectedPeers = HashBiMap.create(); 53 | 54 | // 55 | 56 | public ChatClient(@NonNull Context context) { 57 | mContext = context; 58 | 59 | mProtocol = new BLEProtocol(); 60 | mDataStore = new ContentProviderStore(context); 61 | } 62 | 63 | public void setAirShareServiceBinder(AirShareService.ServiceBinder binder) { 64 | mAirShareServiceBinder = binder; 65 | mAirShareServiceBinder.setCallback(this); 66 | } 67 | 68 | public void setCallback(Callback callback) { 69 | mCallback = callback; 70 | } 71 | 72 | // 73 | 74 | public void makeAvailable() { 75 | if (mDataStore.getPrimaryLocalPeer() == null) { 76 | Timber.e("No primary Identity. Cannot make client available"); 77 | return; 78 | } 79 | 80 | if (mAirShareServiceBinder == null) { 81 | Timber.e("No AirShareBinder set. Cannot make available"); 82 | return; 83 | } 84 | 85 | mAirShareServiceBinder.advertiseLocalUser(); 86 | mAirShareServiceBinder.scanForOtherUsers(); 87 | } 88 | 89 | public void makeUnavailable() { 90 | if (mAirShareServiceBinder == null) { 91 | Timber.e("No AirShareBinder set. Cannot make unavailable"); 92 | return; 93 | } 94 | 95 | mAirShareServiceBinder.stop(); 96 | } 97 | 98 | public Peer getPrimaryLocalPeer() { 99 | return mDataStore.getPrimaryLocalPeer(); 100 | } 101 | 102 | public Peer createPrimaryIdentity(String alias) { 103 | // TODO Test if this should be moved to background thread and async call? 104 | return mDataStore.createLocalPeerWithAlias(alias, mProtocol); 105 | } 106 | 107 | // 108 | 109 | // 110 | 111 | public void sendPublicMessageFromPrimaryIdentity(String body) { 112 | MessagePacket messagePacket = mProtocol.serializeMessage((OwnedIdentityPacket) getPrimaryLocalPeer().getIdentity(), body); 113 | mDataStore.createOrUpdateMessageWithProtocolMessage(messagePacket).close(); 114 | // TODO : Send to connected peers. Future peers will get message during flow 115 | if (mAirShareServiceBinder != null) { 116 | 117 | for (pro.dbro.airshare.session.Peer peer : mConnectedPeers.keySet()) { 118 | ChatPeerFlow flow = mFlows.get(peer); 119 | // If we're actively flowing with a peer, add the message to that flow 120 | // else, send immediately 121 | if (flow != null && !flow.isComplete()) 122 | flow.queueMessage(messagePacket); 123 | else 124 | mAirShareServiceBinder.send(messagePacket.rawPacket, peer); 125 | } 126 | 127 | } 128 | } 129 | 130 | // 131 | 132 | public DataStore getDataStore() { 133 | return mDataStore; 134 | } 135 | 136 | // 137 | 138 | // 139 | 140 | @Override 141 | public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow, 142 | @NonNull Peer remotePeer, 143 | @NonNull ConnectionStatus status) { 144 | 145 | Timber.d("%s %s", remotePeer.getAlias(), status == ConnectionStatus.CONNECTED ? "connected" : "disconnected"); 146 | if (mCallback != null) 147 | mCallback.onAppPeerStatusUpdated(remotePeer, status); 148 | 149 | if (!mAirShareServiceBinder.isActivityReceivingMessages()) 150 | Notification.displayPeerAvailableNotification(mContext, remotePeer, status == ConnectionStatus.CONNECTED); 151 | 152 | switch (status) { 153 | case CONNECTED: 154 | mConnectedPeers.put(flow.getRemoteAirSharePeer(), remotePeer.getId()); 155 | break; 156 | 157 | case DISCONNECTED: 158 | mConnectedPeers.remove(flow.getRemoteAirSharePeer()); 159 | break; 160 | } 161 | } 162 | 163 | @Override 164 | public void onMessageSent(@NonNull ChatPeerFlow flow, @NonNull Message message, @NonNull Peer recipient) { 165 | Timber.d("Sent message: '%s'", message.getBody()); 166 | // TODO : Might be unnecessary 167 | message.close(); 168 | } 169 | 170 | @Override 171 | public void onMessageReceived(@NonNull ChatPeerFlow flow, @NonNull Message message, Peer sender) { 172 | Timber.d("Received message: '%s' with sig '%s' ", message.getBody(), DataUtil.bytesToHex(message.getSignature()).substring(0, 3)); 173 | 174 | // We don't check that mAirShareServiceBinder is not null because this callback is provoked 175 | // by the binder callbacks 176 | 177 | // Send message notification if it's a new message and no Activity is reported active 178 | if (!mAirShareServiceBinder.isActivityReceivingMessages()) { 179 | Notification.displayMessageNotification(mContext, message, sender); 180 | message.close(); 181 | } 182 | } 183 | 184 | @Override 185 | public void onDataRecevied(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer sender, @Nullable Exception exception) { 186 | ChatPeerFlow flow = mFlows.get(sender); 187 | 188 | if (flow == null) { 189 | Timber.w("No flow for %s", sender.getAlias()); 190 | return; 191 | } 192 | 193 | try { 194 | flow.onDataReceived(data); 195 | } catch (ChatPeerFlow.UnexpectedDataException e) { 196 | Timber.e(e, "Error processing received data"); 197 | } 198 | } 199 | 200 | @Override 201 | public void onDataSent(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer recipient, @Nullable Exception exception) { 202 | ChatPeerFlow flow = mFlows.get(recipient); 203 | 204 | if (flow == null) { 205 | Timber.w("No flow for %s", recipient.getAlias()); 206 | return; 207 | } 208 | 209 | try { 210 | flow.onDataSent(data); 211 | } catch (ChatPeerFlow.UnexpectedDataException e) { 212 | Timber.e(e, "Error processing sent data"); 213 | } 214 | } 215 | 216 | @Override 217 | public void onPeerStatusUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, @NonNull Transport.ConnectionStatus newStatus, boolean peerIsHost) { 218 | if (newStatus == Transport.ConnectionStatus.CONNECTED) { 219 | mConnectedPeers.put(peer, null); // We will add the BLEMeshChat peer id after identity is received 220 | Timber.d("Beginning flow with %s as %s", peer.getAlias(), peerIsHost ? "host" : "client"); 221 | mFlows.put(peer, new ChatPeerFlow(mDataStore, mProtocol, this, peer, peerIsHost, this)); 222 | } 223 | else if (newStatus == Transport.ConnectionStatus.DISCONNECTED) { 224 | 225 | if (!mConnectedPeers.containsKey(peer) || mConnectedPeers.get(peer) == null) { 226 | if (mConnectedPeers.containsKey(peer)) mConnectedPeers.remove(peer); 227 | Timber.w("Cannot report peer %s disconnected, no connection record", peer.getAlias()); 228 | return; 229 | } 230 | 231 | int blePeerId = mConnectedPeers.get(peer); 232 | Peer remotePeer = mDataStore.getPeerById(blePeerId); 233 | onAppPeerStatusUpdated(mFlows.get(peer), remotePeer, ConnectionStatus.DISCONNECTED); 234 | } 235 | } 236 | 237 | @Override 238 | public void onPeerTransportUpdated(@NonNull AirShareService.ServiceBinder binder, 239 | @NonNull pro.dbro.airshare.session.Peer peer, 240 | int newTransportCode, 241 | @Nullable Exception exception) { 242 | // unused. The networking demands of this app appear to works fine over BLE 243 | } 244 | 245 | @Override 246 | public void sendData(pro.dbro.airshare.session.Peer peer, byte[] data) { 247 | if(mAirShareServiceBinder == null) { 248 | Timber.e("AirShare Service binder is null! Cannot send data"); 249 | return; 250 | } 251 | mAirShareServiceBinder.send(data, peer); 252 | } 253 | 254 | // 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ChatPeerFlow.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import java.util.ArrayDeque; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import pro.dbro.airshare.session.Peer; 11 | import pro.dbro.ble.data.DataStore; 12 | import pro.dbro.ble.data.model.DataUtil; 13 | import pro.dbro.ble.data.model.Message; 14 | import pro.dbro.ble.data.model.MessageCollection; 15 | import pro.dbro.ble.protocol.IdentityPacket; 16 | import pro.dbro.ble.protocol.MessagePacket; 17 | import pro.dbro.ble.protocol.NoDataPacket; 18 | import pro.dbro.ble.protocol.OwnedIdentityPacket; 19 | import pro.dbro.ble.protocol.Protocol; 20 | import timber.log.Timber; 21 | 22 | /** 23 | * This class orchestrates the flow between two ChatApp Peers, handing network requests and 24 | * updating the {@link pro.dbro.ble.data.DataStore}. The client of this class may use 25 | * {@link ChatPeerFlow.Callback} to update their UI or in-memory application state. 26 | * 27 | * The general gist of the flow: 28 | * 29 | * 1) Client peer writes identity 30 | * 2) Client peer waits for host identity 31 | * 3) Client peer writes outgoing messages 32 | * 4) Client peer waits for incoming messages 33 | * Created by davidbrodsky on 4/16/15. 34 | */ 35 | public class ChatPeerFlow { 36 | 37 | public static class UnexpectedDataException extends Exception { 38 | public UnexpectedDataException(String detailMessage) { 39 | super(detailMessage); 40 | } 41 | } 42 | 43 | /** Entity responsible for sending data to a peer */ 44 | public static interface DataOutlet { 45 | public void sendData(Peer peer, byte[] data); 46 | } 47 | 48 | public static interface Callback { 49 | 50 | public static enum ConnectionStatus { CONNECTED, DISCONNECTED } 51 | 52 | public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow, 53 | @NonNull pro.dbro.ble.data.model.Peer peer, 54 | @NonNull ConnectionStatus status); 55 | 56 | public void onMessageSent(@NonNull ChatPeerFlow flow, 57 | @NonNull Message message, 58 | @NonNull pro.dbro.ble.data.model.Peer recipient); 59 | 60 | public void onMessageReceived(@NonNull ChatPeerFlow flow, 61 | @NonNull Message message, 62 | @Nullable pro.dbro.ble.data.model.Peer sender); 63 | 64 | } 65 | 66 | private static final int MESSAGES_PER_RESPONSE = 50; 67 | private static final int IDENTITIES_PER_RESPONSE = 10; 68 | public static enum State { CLIENT_WRITE_ID, HOST_WRITE_ID, CLIENT_WRITE_MSGS, HOST_WRITE_MSGS } 69 | 70 | private State mState = State.CLIENT_WRITE_ID; 71 | private OwnedIdentityPacket mLocalIdentity; 72 | private Peer mRemoteAirSharePeer; 73 | private Protocol mProtocol; 74 | private DataStore mDataStore; 75 | private DataOutlet mOutlet; 76 | private IdentityPacket mRemoteIdentity; 77 | private Callback mCallback; 78 | private ArrayDeque mMessageOutbox = new ArrayDeque<>(); 79 | private ArrayDeque mIdentityOutbox = new ArrayDeque<>(); 80 | 81 | private boolean mPeerIsHost; 82 | private boolean mIsComplete = false; 83 | private boolean mFetchedMessages = false; 84 | private boolean mFetchedIdentities = false; 85 | private boolean mGotRemotePeerIdentity = false; 86 | 87 | public ChatPeerFlow(DataStore dataStore, 88 | Protocol protocol, 89 | DataOutlet outlet, 90 | Peer remotePeer, 91 | boolean peerIsHost, 92 | Callback callback) { 93 | 94 | mRemoteAirSharePeer = remotePeer; 95 | mOutlet = outlet; 96 | mProtocol = protocol; 97 | mDataStore = dataStore; 98 | mLocalIdentity = (OwnedIdentityPacket) dataStore.getPrimaryLocalPeer().getIdentity(); 99 | mPeerIsHost = peerIsHost; 100 | mCallback = callback; 101 | 102 | // Client initiates flow 103 | if (mPeerIsHost) 104 | sendIdentity(); 105 | } 106 | 107 | public boolean isComplete() { 108 | return mIsComplete; 109 | } 110 | 111 | public Peer getRemoteAirSharePeer() { 112 | return mRemoteAirSharePeer; 113 | } 114 | 115 | public void queueMessage(MessagePacket message) { 116 | mMessageOutbox.add(message); 117 | } 118 | 119 | /** 120 | * Called when data is acknowledged as sent to the peer passed to this instance's constructor 121 | * @return whether this flow is complete and should not receive further events. 122 | */ 123 | public boolean onDataSent(byte[] data) throws UnexpectedDataException { 124 | // When data is ack'd we should be in a local-peer writing state 125 | if ((!mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete))) || 126 | (mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete)))) { 127 | 128 | throw new IllegalStateException(String.format("onDataSent invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host")); 129 | 130 | } 131 | 132 | Timber.d("Sent data %s", DataUtil.bytesToHex(data)); 133 | 134 | byte type = mProtocol.getPacketType(data); 135 | 136 | // TODO : Perhaps we should cache last sent item to avoid deserializing bytes we've 137 | // just serialized in sendData 138 | switch (mState) { 139 | case HOST_WRITE_ID: 140 | case CLIENT_WRITE_ID: 141 | 142 | switch(type) { 143 | case IdentityPacket.TYPE: 144 | 145 | IdentityPacket sentIdPkt = mProtocol.deserializeIdentity(data); 146 | mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(sentIdPkt); 147 | // We can only report the identity sent once we know the peer's identity 148 | // We also always want to send our own identity first 149 | if (mRemoteIdentity != null) { 150 | Timber.d("Marked identity %s delivered to %s", sentIdPkt.alias, mRemoteIdentity.alias); 151 | mDataStore.markIdentityDeliveredToPeer(sentIdPkt, mRemoteIdentity); 152 | } 153 | 154 | mIdentityOutbox.poll(); 155 | 156 | sendAsAppropriate(); 157 | break; 158 | 159 | case NoDataPacket.TYPE: 160 | 161 | incrementStateAndSendAsAppropriate(); 162 | break; 163 | 164 | default: 165 | throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type)); 166 | 167 | } 168 | break; 169 | 170 | case HOST_WRITE_MSGS: 171 | case CLIENT_WRITE_MSGS: 172 | 173 | switch(type) { 174 | case MessagePacket.TYPE: 175 | 176 | MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity); 177 | Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt); 178 | // Mark incoming messages as delivered to sender 179 | mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity); 180 | mCallback.onMessageSent(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey)); 181 | 182 | mMessageOutbox.poll(); 183 | 184 | sendAsAppropriate(); 185 | break; 186 | 187 | case NoDataPacket.TYPE: 188 | 189 | incrementStateAndSendAsAppropriate(); 190 | break; 191 | 192 | default: 193 | throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type)); 194 | 195 | } 196 | 197 | break; 198 | 199 | default: 200 | Timber.e("Flow received unexpected response from client peer"); 201 | } 202 | return mIsComplete; 203 | } 204 | 205 | /** 206 | * Called when data is received from the peer passed to this instance's constructor 207 | * @return whether this flow is complete and should not receive further events. 208 | */ 209 | public boolean onDataReceived(byte[] data) throws UnexpectedDataException { 210 | // When data comes in we should be in a remote-peer writing state 211 | if ((!mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete))) || 212 | (mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete)))) { 213 | 214 | throw new IllegalStateException(String.format("onDataReceived invalid state %s for local as %s", mState, mPeerIsHost ? "client" : "host")); 215 | 216 | } 217 | 218 | //Timber.d("Received data %s", DataUtil.bytesToHex(data)); 219 | 220 | byte type = mProtocol.getPacketType(data); 221 | 222 | switch (mState) { 223 | case HOST_WRITE_ID: 224 | case CLIENT_WRITE_ID: 225 | 226 | switch(type) { 227 | case IdentityPacket.TYPE: 228 | 229 | mRemoteIdentity = mProtocol.deserializeIdentity(data); 230 | Timber.d("Got remote identity for %s", mRemoteIdentity.alias); 231 | pro.dbro.ble.data.model.Peer remotePeer = mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(mRemoteIdentity); 232 | // Only treat first identity as that of connected peer 233 | if (!mGotRemotePeerIdentity) { 234 | mCallback.onAppPeerStatusUpdated(this, remotePeer, Callback.ConnectionStatus.CONNECTED); 235 | mGotRemotePeerIdentity = true; 236 | } 237 | break; 238 | 239 | case NoDataPacket.TYPE: 240 | 241 | Timber.d("Received identity NoData"); 242 | incrementStateAndSendAsAppropriate(); 243 | break; 244 | 245 | default: 246 | 247 | throw new UnexpectedDataException(String.format("Expected IdentityPacket (type %d). Got type %d", IdentityPacket.TYPE, type)); 248 | } 249 | 250 | break; 251 | 252 | case HOST_WRITE_MSGS: 253 | case CLIENT_WRITE_MSGS: 254 | 255 | switch (type) { 256 | case MessagePacket.TYPE: 257 | 258 | MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity); 259 | Timber.d("Received msg %s", msgPkt.body); 260 | 261 | // Mark incoming messages as delivered to sender 262 | 263 | boolean isNewMessage = true; 264 | Message existingMessage = mDataStore.getMessageBySignature(msgPkt.signature); 265 | if (existingMessage != null) { 266 | isNewMessage = false; 267 | existingMessage.close(); 268 | } 269 | 270 | // TODO : Allow updating a message? 271 | Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt); 272 | mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity); 273 | 274 | if (isNewMessage) 275 | mCallback.onMessageReceived(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey)); 276 | 277 | break; 278 | 279 | case NoDataPacket.TYPE: 280 | 281 | Timber.d("Received msg NoData"); 282 | incrementStateAndSendAsAppropriate(); 283 | break; 284 | 285 | default: 286 | 287 | throw new UnexpectedDataException(String.format("Expected MessagePacket (type %d). Got type %d", MessagePacket.TYPE, type)); 288 | 289 | } 290 | break; 291 | 292 | default: 293 | Timber.e("Flow received unexpected response from client peer"); 294 | } 295 | return mIsComplete; 296 | } 297 | 298 | private void sendIdentity() { 299 | if (!mFetchedIdentities) { 300 | 301 | // If we're the client, we're initiating the identity flow, and we won't have the remote identity yet 302 | mIdentityOutbox.addAll(getIdentitiesForIdentity(mRemoteIdentity == null ? null : mRemoteIdentity.publicKey, 303 | IDENTITIES_PER_RESPONSE)); 304 | mFetchedIdentities = true; 305 | } 306 | 307 | Timber.d("Send identity %s", mIdentityOutbox.size() == 0 ? "NoData" : ""); 308 | mOutlet.sendData(mRemoteAirSharePeer, 309 | mIdentityOutbox.size() == 0 ? 310 | mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket : 311 | mIdentityOutbox.peek().rawPacket); 312 | } 313 | 314 | private void sendMessage() { 315 | if (!mFetchedMessages) { 316 | mMessageOutbox.addAll(getMessagesForIdentity(mRemoteIdentity.publicKey, MESSAGES_PER_RESPONSE)); 317 | mFetchedMessages = true; 318 | } 319 | 320 | Timber.d("Send message %s", mMessageOutbox.size() == 0 ? "NoData" : ""); 321 | mOutlet.sendData(mRemoteAirSharePeer, 322 | mMessageOutbox.size() == 0 ? 323 | mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket : 324 | mMessageOutbox.peek().rawPacket); 325 | } 326 | 327 | private void incrementStateAndSendAsAppropriate() { 328 | if (mState == State.HOST_WRITE_MSGS) { 329 | Timber.d("ChatPeerFlow complete!"); 330 | mIsComplete = true; 331 | return; 332 | } 333 | 334 | mState = State.values()[mState.ordinal() + 1]; 335 | Timber.d("ChatPeerFlow New State : %s", mState); 336 | sendAsAppropriate(); 337 | } 338 | 339 | private void sendAsAppropriate() { 340 | 341 | switch (mState) { 342 | case CLIENT_WRITE_ID: 343 | if (mPeerIsHost) sendIdentity(); 344 | break; 345 | 346 | case HOST_WRITE_ID: 347 | if (!mPeerIsHost) sendIdentity(); 348 | break; 349 | 350 | case CLIENT_WRITE_MSGS: 351 | if (mPeerIsHost) sendMessage(); 352 | break; 353 | 354 | case HOST_WRITE_MSGS: 355 | if (!mPeerIsHost) sendMessage(); 356 | break; 357 | } 358 | } 359 | 360 | /** 361 | * Return a queue of message packets for delivery to remote identity with given public key. 362 | * 363 | * If recipientPublicKey is null, queues most recent messages 364 | */ 365 | private ArrayDeque getMessagesForIdentity(@Nullable byte[] recipientPublicKey, int maxMessages) { 366 | ArrayDeque messagePacketQueue = new ArrayDeque<>(); 367 | 368 | if (recipientPublicKey != null) { 369 | // Get messages not delievered to peer 370 | pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey); 371 | List messages = mDataStore.getOutgoingMessagesForPeer(recipient, maxMessages); 372 | 373 | if (messages == null || messages.size() == 0) { 374 | Timber.d("Got no messages for peer with pub key " + DataUtil.bytesToHex(recipientPublicKey)); 375 | } else { 376 | messagePacketQueue.addAll(messages); 377 | } 378 | } else { 379 | // Get most recent messages 380 | MessageCollection recentMessages = mDataStore.getRecentMessages(); 381 | for (int x = 0; x < Math.min(maxMessages, recentMessages.getCursor().getCount()); x++) { 382 | Message currentMessage = recentMessages.getMessageAtPosition(x); 383 | if (currentMessage != null) 384 | messagePacketQueue.add(currentMessage.getProtocolMessage(mDataStore)); 385 | } 386 | recentMessages.close(); 387 | } 388 | return messagePacketQueue; 389 | } 390 | 391 | /** 392 | * Return a queue of identity packets for delivery to the remote identity with the given 393 | * public key. 394 | * 395 | * If recipientPublicKey is null, or no messages undelivered for recipient, 396 | * the user identity will be queued. As such this method will never return a null 397 | * or empty queue. Thus it should only be called once per flow and should not 398 | * be used as an indication of whether identity transmission with a peer is complete. 399 | */ 400 | private ArrayDeque getIdentitiesForIdentity(@Nullable byte[] recipientPublicKey, int maxIdentities) { 401 | List identities = null; 402 | ArrayDeque identityPacketQueue = new ArrayDeque<>(); 403 | if (recipientPublicKey != null) { 404 | // We have a public key for the remote peer, fetch undelivered identities 405 | pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey); 406 | identities = mDataStore.getOutgoingIdentitiesForPeer(recipient, maxIdentities); 407 | } 408 | 409 | if (identities == null || identities.size() == 0) { 410 | Timber.d("Got no identities to send for peer %s. Sending own identity", recipientPublicKey == null ? "" : "with pub key " + DataUtil.bytesToHex(recipientPublicKey).substring(2, 6)); 411 | // For now, at least send our identity 412 | if (identities == null) identities = new ArrayList<>(1); 413 | identities.add(mDataStore.getPrimaryLocalPeer().getIdentity()); 414 | } 415 | identityPacketQueue.addAll(identities); 416 | 417 | return identityPacketQueue; 418 | } 419 | 420 | } 421 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/PrefsManager.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble; 2 | 3 | import android.content.Context; 4 | 5 | 6 | /** 7 | * Created by davidbrodsky on 9/21/14. 8 | */ 9 | public class PrefsManager { 10 | 11 | /** SharedPreferences store names */ 12 | private static final String APP_PREFS = "prefs"; 13 | 14 | /** SharedPreferences keys */ 15 | private static final String APP_STATUS = "status"; 16 | 17 | public static int getStatus(Context context) { 18 | return context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE) 19 | .getInt(APP_STATUS, 0); 20 | } 21 | 22 | public static void setStatus(Context context, int status) { 23 | context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit() 24 | .putInt(APP_STATUS, status) 25 | .commit(); 26 | } 27 | 28 | public static void clearState(Context context) { 29 | context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit().clear().apply(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/crypto/KeyPair.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.crypto; 2 | 3 | /** 4 | * Created by davidbrodsky on 10/22/14. 5 | */ 6 | public class KeyPair { 7 | 8 | public final byte[] publicKey; 9 | public final byte[] secretKey; 10 | 11 | public KeyPair(byte[] publicKey, byte[] secretKey) { 12 | this.publicKey = publicKey; 13 | this.secretKey = secretKey; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/crypto/SodiumShaker.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.crypto; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import org.abstractj.kalium.NaCl; 6 | import org.abstractj.kalium.Sodium; 7 | 8 | /** 9 | * Wrapper around libsodium functions. 10 | * 11 | * Created by davidbrodsky on 10/13/14. 12 | */ 13 | public class SodiumShaker { 14 | private static final String TAG = "Identity"; 15 | 16 | public static final int crypto_sign_PUBLICKEYBYTES = 32; 17 | private static final int crypto_sign_SECRETKEYBYTES = 64; 18 | public static final int crypto_sign_BYTES = 64; 19 | 20 | static { 21 | // Load native libraries 22 | NaCl.sodium(); 23 | // Initialize libsodium 24 | if (Sodium.sodium_init() == -1) { 25 | throw new IllegalStateException("sodiun_init failed!"); 26 | } 27 | } 28 | 29 | public static KeyPair generateKeyPair() { 30 | byte[] pk = new byte[crypto_sign_PUBLICKEYBYTES]; 31 | byte[] sk = new byte[crypto_sign_SECRETKEYBYTES]; 32 | 33 | Sodium.crypto_sign_ed25519_keypair(pk, sk); 34 | return new KeyPair(pk, sk); 35 | } 36 | 37 | public static byte[] generateSignatureForMessage(@NonNull byte[] secret_key, @NonNull byte[] message, int message_len) { 38 | if (secret_key.length != crypto_sign_SECRETKEYBYTES) throw new IllegalArgumentException("secret_key is incorrect length"); 39 | byte[] signature = new byte[crypto_sign_BYTES]; 40 | int[] signature_len = new int[0]; 41 | 42 | Sodium.crypto_sign_ed25519_detached(signature, signature_len, message, message_len, secret_key); 43 | 44 | return signature; 45 | } 46 | 47 | /** 48 | * Very that signature and public_key verify message 49 | * 50 | * @param public_key the public key corresponding to signature 51 | * @param signature the signature of message decipherable with public_key 52 | * @param message the data with signature 53 | */ 54 | public static boolean verifySignature(@NonNull byte[] public_key, @NonNull byte[] signature, @NonNull byte[] message) { 55 | // Verify signature 56 | 57 | if (Sodium.crypto_sign_ed25519_verify_detached(signature, message, message.length, public_key) != 0) { 58 | /* Incorrect signature! */ 59 | return false; 60 | } 61 | return true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/ContentProviderStore.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data; 2 | 3 | import android.content.ContentValues; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.support.annotation.NonNull; 8 | import android.support.annotation.Nullable; 9 | import android.util.Log; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Date; 13 | import java.util.List; 14 | 15 | import pro.dbro.ble.crypto.KeyPair; 16 | import pro.dbro.ble.crypto.SodiumShaker; 17 | import pro.dbro.ble.data.model.ChatContentProvider; 18 | import pro.dbro.ble.data.model.DataUtil; 19 | import pro.dbro.ble.data.model.IdentityDeliveryTable; 20 | import pro.dbro.ble.data.model.Message; 21 | import pro.dbro.ble.data.model.MessageCollection; 22 | import pro.dbro.ble.data.model.MessageDeliveryTable; 23 | import pro.dbro.ble.data.model.MessageTable; 24 | import pro.dbro.ble.data.model.Peer; 25 | import pro.dbro.ble.data.model.PeerTable; 26 | import pro.dbro.ble.protocol.IdentityPacket; 27 | import pro.dbro.ble.protocol.MessagePacket; 28 | import pro.dbro.ble.protocol.OwnedIdentityPacket; 29 | import pro.dbro.ble.protocol.Protocol; 30 | 31 | /** 32 | * API for the application's data persistence 33 | * 34 | * If the underlying data storage were to be replaced, this should be the 35 | * only class requiring modification. 36 | * 37 | * Created by davidbrodsky on 10/20/14. 38 | */ 39 | public class ContentProviderStore extends DataStore { 40 | public static final String TAG = "DataManager"; 41 | 42 | public ContentProviderStore(Context context) { 43 | super(context); 44 | } 45 | 46 | @Override 47 | public void markMessageDeliveredToPeer(@NonNull MessagePacket messagePacket, @NonNull IdentityPacket recipientPacket) { 48 | Message message = getMessageBySignature(messagePacket.signature); 49 | Peer recipient = getPeerByPubKey(recipientPacket.publicKey); 50 | 51 | if (message == null || recipient == null) { 52 | Log.w(TAG, "Unable to record message delivery. No peer or message database id available"); 53 | return; 54 | } 55 | 56 | ContentValues delivery = new ContentValues(); 57 | delivery.put(MessageDeliveryTable.messageId, message.getId()); 58 | delivery.put(MessageDeliveryTable.peerId, recipient.getId()); 59 | 60 | mContext.getContentResolver().insert(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, delivery); 61 | Log.i(TAG, "Recorded message delivery"); 62 | message.close(); 63 | } 64 | 65 | @Override 66 | public void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity) { 67 | Peer payloadPeer = getPeerByPubKey(payloadIdentity.publicKey); 68 | Peer recipientPeer = getPeerByPubKey(recipientIdentity.publicKey); 69 | 70 | if (payloadPeer == null || recipientPeer == null) { 71 | Log.w(TAG, "Unable to fetch payload or recipient identity. Cannot mark identity delivered"); 72 | return; 73 | } 74 | 75 | ContentValues delivery = new ContentValues(); 76 | delivery.put(IdentityDeliveryTable.peerPayloadId, payloadPeer.getId()); 77 | delivery.put(IdentityDeliveryTable.peerRecipientId, recipientPeer.getId()); 78 | 79 | mContext.getContentResolver().insert(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES, delivery); 80 | Log.i(TAG, "Recorded identity delivery"); 81 | } 82 | 83 | @Nullable 84 | @Override 85 | public Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol) { 86 | KeyPair keyPair = SodiumShaker.generateKeyPair(); 87 | ContentValues dbEntry = new ContentValues(); 88 | dbEntry.put(PeerTable.pubKey, keyPair.publicKey); 89 | dbEntry.put(PeerTable.secKey, keyPair.secretKey); 90 | dbEntry.put(PeerTable.alias, alias); 91 | dbEntry.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date())); 92 | if (protocol != null) { 93 | // If protocol is available, use it to cache the Identity packet for transmission 94 | dbEntry.put(PeerTable.rawPkt, protocol.serializeIdentity( 95 | new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, alias, null))); 96 | } 97 | Uri newIdentityUri = mContext.getContentResolver().insert(ChatContentProvider.Peers.PEERS, dbEntry); 98 | return getPeerById(Integer.parseInt(newIdentityUri.getLastPathSegment())); 99 | } 100 | 101 | /** 102 | * @return the first user peer entry in the database, 103 | * or null if no identity is set. 104 | */ 105 | @Override 106 | @Nullable 107 | public Peer getPrimaryLocalPeer() { 108 | // TODO: caching 109 | Cursor result = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS, 110 | null, 111 | PeerTable.secKey + " IS NOT NULL", 112 | null, 113 | null); 114 | if (result != null && result.moveToFirst()) { 115 | Peer peer = new Peer(result); 116 | result.close(); 117 | return peer; 118 | } 119 | return null; 120 | } 121 | 122 | @Nullable 123 | @Override 124 | public List getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages) { 125 | // TODO : Don't send messages past a certain age etc? 126 | Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null, null, null, null); 127 | if (messagesCursor != null) { 128 | List messagesToSend = new ArrayList<>(); 129 | while (messagesCursor.moveToNext()) { 130 | Message individualMessage = new Message(messagesCursor); 131 | if (!haveDeliveredMessageToPeer(individualMessage, recipient)) { 132 | messagesToSend.add(individualMessage.getProtocolMessage(this)); 133 | if (messagesToSend.size() > maxMessages) break; 134 | } 135 | } 136 | 137 | messagesCursor.close(); 138 | return messagesToSend; 139 | } 140 | return null; 141 | } 142 | 143 | @Override 144 | public List getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxIdentities) { 145 | // TODO : Don't send identities past a certain age etc? 146 | Cursor identitiesCursor = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS, null, null, null, null); 147 | if (identitiesCursor != null) { 148 | List identitiesToSend = new ArrayList<>(); 149 | while (identitiesCursor.moveToNext()) { 150 | Peer payloadPeer = new Peer(identitiesCursor); 151 | if (!haveDeliveredPeerIdentityToPeer(payloadPeer, recipient)) { 152 | identitiesToSend.add(payloadPeer.getIdentity()); 153 | if (identitiesToSend.size() > maxIdentities) break; 154 | } 155 | 156 | } 157 | 158 | identitiesCursor.close(); 159 | return identitiesToSend; 160 | } 161 | return null; 162 | } 163 | 164 | @Override 165 | public MessageCollection getRecentMessages() { 166 | Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, 167 | null, 168 | null, 169 | null, 170 | MessageTable.receivedDate + " DESC"); 171 | 172 | if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) { 173 | return new MessageCollection(messagesCursor); 174 | } 175 | return null; 176 | } 177 | 178 | @Override 179 | public MessageCollection getRecentMessagesByPeer(@NonNull Peer author) { 180 | Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, 181 | null, 182 | MessageTable.peerId + "=?", 183 | new String[] { String.valueOf(author.getId()) }, 184 | MessageTable.receivedDate + " DESC"); 185 | 186 | if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) { 187 | return new MessageCollection(messagesCursor); 188 | } 189 | return null; 190 | } 191 | 192 | @Nullable 193 | @Override 194 | public Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket remoteIdentityPacket) { 195 | // Query if peer exists 196 | Peer peer = getPeerByPubKey(remoteIdentityPacket.publicKey); 197 | 198 | ContentValues peerValues = new ContentValues(); 199 | peerValues.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date())); 200 | peerValues.put(PeerTable.pubKey, remoteIdentityPacket.publicKey); 201 | peerValues.put(PeerTable.alias, remoteIdentityPacket.alias); 202 | peerValues.put(PeerTable.rawPkt, remoteIdentityPacket.rawPacket); 203 | 204 | if (peer != null) { 205 | // Peer exists. Modify lastSeenDate 206 | Log.i(TAG, "Updating peer for pubkey " + DataUtil.bytesToHex(remoteIdentityPacket.publicKey)); 207 | 208 | int updated = mContext.getContentResolver().update( 209 | ChatContentProvider.Peers.PEERS, 210 | peerValues, 211 | "quote("+ PeerTable.pubKey + ") = ?" , 212 | new String[] {DataUtil.bytesToHex(remoteIdentityPacket.publicKey)}); 213 | if (updated != 1) { 214 | Log.e(TAG, "Failed to update peer last seen"); 215 | } 216 | } else { 217 | // Peer does not exist. Create. 218 | Uri peerUri = mContext.getContentResolver().insert( 219 | ChatContentProvider.Peers.PEERS, 220 | peerValues); 221 | 222 | // Fetch newly created peer 223 | peer = getPeerById(Integer.parseInt(peerUri.getLastPathSegment())); 224 | Log.i(TAG, String.format("Created new peer %d for pubkey %s", Integer.parseInt(peerUri.getLastPathSegment()), DataUtil.bytesToHex(remoteIdentityPacket.publicKey))); 225 | 226 | if (peer == null) { 227 | Log.e(TAG, "Failed to query peer after insertion."); 228 | } 229 | } 230 | return peer; 231 | } 232 | 233 | @Nullable 234 | @Override 235 | public Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket) { 236 | // Query if peer exists 237 | Peer peer = getPeerByPubKey(protocolMessagePacket.sender.publicKey); 238 | 239 | if (peer == null) 240 | throw new IllegalStateException("Failed to get peer for message"); 241 | 242 | // See if message exists 243 | Message message = getMessageBySignature(protocolMessagePacket.signature); 244 | if (message == null) { 245 | // Message doesn't exist in our database 246 | 247 | // Insert message into database 248 | ContentValues newMessageEntry = new ContentValues(); 249 | newMessageEntry.put(MessageTable.body, protocolMessagePacket.body); 250 | newMessageEntry.put(MessageTable.peerId, peer.getId()); 251 | newMessageEntry.put(MessageTable.receivedDate, DataUtil.storedDateFormatter.format(new Date())); 252 | newMessageEntry.put(MessageTable.authoredDate, DataUtil.storedDateFormatter.format(protocolMessagePacket.authoredDate)); 253 | newMessageEntry.put(MessageTable.signature, protocolMessagePacket.signature); 254 | newMessageEntry.put(MessageTable.replySig, protocolMessagePacket.replySig); 255 | newMessageEntry.put(MessageTable.rawPacket, protocolMessagePacket.rawPacket); 256 | 257 | Uri newMessageUri = mContext.getContentResolver().insert( 258 | ChatContentProvider.Messages.MESSAGES, 259 | newMessageEntry); 260 | message = getMessageById(Integer.parseInt(newMessageUri.getLastPathSegment())); 261 | } else { 262 | // We already have a message with this signature 263 | // Since we currently don't have any mutable message fields (e.g hopcount) 264 | // do nothing 265 | Log.i(TAG, "Received stored message. Ignoring"); 266 | } 267 | 268 | return message; 269 | } 270 | 271 | @Nullable 272 | @Override 273 | public Message getMessageBySignature(@NonNull byte[] signature) { 274 | Cursor messageCursor = mContext.getContentResolver().query( 275 | ChatContentProvider.Messages.MESSAGES, 276 | null, 277 | "quote(" + MessageTable.signature + ") = ?", 278 | new String[] {DataUtil.bytesToHex(signature)}, 279 | null); 280 | if (messageCursor != null && messageCursor.moveToFirst()) { 281 | return new Message(messageCursor); 282 | } 283 | return null; 284 | } 285 | 286 | @Nullable 287 | @Override 288 | public Message getMessageById(int id) { 289 | Cursor messageCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null, 290 | MessageTable.id + " = ?", 291 | new String[]{String.valueOf(id)}, 292 | null); 293 | if (messageCursor != null && messageCursor.moveToFirst()) { 294 | return new Message(messageCursor); 295 | } 296 | return null; 297 | } 298 | 299 | @Nullable 300 | @Override 301 | public Peer getPeerByPubKey(@NonNull byte[] publicKey) { 302 | Cursor peerCursor = mContext.getContentResolver().query( 303 | ChatContentProvider.Peers.PEERS, 304 | null, 305 | "quote(" + PeerTable.pubKey + ") = ?", 306 | new String[] {DataUtil.bytesToHex(publicKey)}, 307 | null); 308 | if (peerCursor != null && peerCursor.moveToFirst()) { 309 | Peer peer = new Peer(peerCursor); 310 | peerCursor.close(); 311 | return peer; 312 | } 313 | return null; 314 | } 315 | 316 | @Nullable 317 | @Override 318 | public Peer getPeerById(int id) { 319 | Cursor peerCursor = mContext.getContentResolver().query( 320 | ChatContentProvider.Peers.PEERS, 321 | null, 322 | PeerTable.id + " = ?", 323 | new String[] {String.valueOf(id)}, 324 | null); 325 | if (peerCursor != null && peerCursor.moveToFirst()) { 326 | Peer peer = new Peer(peerCursor); 327 | peerCursor.close(); 328 | return peer; 329 | } 330 | return null; 331 | } 332 | 333 | @Override 334 | public int countPeers() { 335 | Cursor peerCursor = mContext.getContentResolver().query( 336 | ChatContentProvider.Peers.PEERS, 337 | new String[] {PeerTable.id}, 338 | null, 339 | null, 340 | null); 341 | if (peerCursor != null) { 342 | int result = peerCursor.getCount(); 343 | peerCursor.close(); 344 | return result; 345 | } 346 | return 0; 347 | } 348 | 349 | @Override 350 | public int countMessagesPassed() { 351 | Cursor deliveryCursor = mContext.getContentResolver().query( 352 | ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, 353 | new String[] {MessageDeliveryTable.id}, 354 | null, 355 | null, 356 | null); 357 | if (deliveryCursor != null) { 358 | int result = deliveryCursor.getCount(); 359 | deliveryCursor.close(); 360 | return result; 361 | } 362 | return 0; 363 | } 364 | 365 | /** Utility */ 366 | 367 | private boolean haveDeliveredMessageToPeer(Message message, Peer peer) { 368 | Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, 369 | null, 370 | MessageDeliveryTable.messageId + " = ? AND " + MessageDeliveryTable.peerId + " = ?", 371 | new String[]{String.valueOf(message.getId()), String.valueOf(peer.getId())}, 372 | null); 373 | try { 374 | return deliveryCursor != null && deliveryCursor.moveToFirst(); 375 | } finally { 376 | if (deliveryCursor != null) deliveryCursor.close(); 377 | } 378 | } 379 | 380 | /** 381 | * @return whether peerPayload has been delivered to peerRecipient 382 | */ 383 | private boolean haveDeliveredPeerIdentityToPeer(Peer peerPayload, Peer peerRecipient) { 384 | Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES, 385 | null, 386 | IdentityDeliveryTable.peerRecipientId + " = ? AND " + IdentityDeliveryTable.peerPayloadId + " = ?", 387 | new String[]{String.valueOf(peerRecipient.getId()), String.valueOf(peerPayload.getId())}, 388 | null); 389 | try { 390 | return deliveryCursor != null && deliveryCursor.moveToFirst(); 391 | } finally { 392 | if (deliveryCursor != null) deliveryCursor.close(); 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/DataStore.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | 7 | import java.util.List; 8 | 9 | import pro.dbro.ble.data.model.Message; 10 | import pro.dbro.ble.data.model.MessageCollection; 11 | import pro.dbro.ble.data.model.Peer; 12 | import pro.dbro.ble.protocol.IdentityPacket; 13 | import pro.dbro.ble.protocol.MessagePacket; 14 | import pro.dbro.ble.protocol.Protocol; 15 | 16 | /** 17 | * Data persistence layer. Any data storage mechanism 18 | * needs to implement this interface. 19 | * 20 | * Created by davidbrodsky on 10/20/14. 21 | */ 22 | public abstract class DataStore { 23 | 24 | protected Context mContext; 25 | 26 | public DataStore(@NonNull Context context) { 27 | mContext = context.getApplicationContext(); 28 | } 29 | 30 | public abstract void markMessageDeliveredToPeer(@NonNull MessagePacket message, @NonNull IdentityPacket recipient); 31 | 32 | public abstract void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity); 33 | 34 | public abstract Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol); 35 | 36 | public abstract Peer getPrimaryLocalPeer(); 37 | 38 | public abstract List getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages); 39 | 40 | public abstract List getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxMessages); 41 | 42 | public abstract MessageCollection getRecentMessages(); 43 | 44 | public abstract MessageCollection getRecentMessagesByPeer(@NonNull Peer author); 45 | 46 | public abstract Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket identityPacket); 47 | 48 | public abstract Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket); 49 | 50 | public abstract Message getMessageBySignature(@NonNull byte[] signature); 51 | 52 | public abstract Message getMessageById(int id); 53 | 54 | public abstract Peer getPeerByPubKey(@NonNull byte[] publicKey); 55 | 56 | public abstract Peer getPeerById(int id); 57 | 58 | public abstract int countPeers(); 59 | 60 | public abstract int countMessagesPassed(); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/ChatContentProvider.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import android.net.Uri; 4 | 5 | import net.simonvt.schematic.annotation.ContentProvider; 6 | import net.simonvt.schematic.annotation.ContentUri; 7 | import net.simonvt.schematic.annotation.TableEndpoint; 8 | 9 | /** 10 | * ContentProvider definition. This defines a familiar API 11 | * for Android framework components to utilize. 12 | * 13 | * Created by davidbrodsky on 7/28/14. 14 | */ 15 | @ContentProvider(authority = ChatContentProvider.AUTHORITY, database = ChatDatabase.class) 16 | public final class ChatContentProvider { 17 | 18 | public static final String AUTHORITY = "pro.dbro.ble.chatprovider"; 19 | private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); 20 | 21 | private static Uri buildUri(String... paths) { 22 | Uri.Builder builder = BASE_CONTENT_URI.buildUpon(); 23 | for (String path : paths) { 24 | builder.appendPath(path); 25 | } 26 | return builder.build(); 27 | } 28 | 29 | /** Peer API **/ 30 | 31 | @TableEndpoint(table = ChatDatabase.PEERS) 32 | public static class Peers { 33 | 34 | private static final String ENDPOINT = "peers"; 35 | 36 | @ContentUri( 37 | path = ENDPOINT, 38 | type = "vnd.android.cursor.dir/list", 39 | defaultSort = PeerTable.alias + " ASC") 40 | public static final Uri PEERS = buildUri(ENDPOINT); 41 | } 42 | 43 | /** Messages API **/ 44 | 45 | @TableEndpoint(table = ChatDatabase.MESSAGES) 46 | public static class Messages { 47 | 48 | private static final String ENDPOINT = "msgs"; 49 | 50 | @ContentUri( 51 | path = ENDPOINT, 52 | type = "vnd.android.cursor.dir/list", 53 | defaultSort = MessageTable.authoredDate + " ASC") 54 | public static final Uri MESSAGES = buildUri(ENDPOINT); 55 | 56 | } 57 | 58 | /** MessageDelivery API **/ 59 | 60 | @TableEndpoint(table = ChatDatabase.DELIVERED_MESSAGES) 61 | public static class MessageDeliveries { 62 | 63 | private static final String ENDPOINT = "message_deliveries"; 64 | 65 | @ContentUri( 66 | path = ENDPOINT, 67 | type = "vnd.android.cursor.dir/list", 68 | defaultSort = MessageDeliveryTable.messageId + " ASC") 69 | public static final Uri MESSAGE_DELIVERIES = buildUri(ENDPOINT); 70 | 71 | } 72 | 73 | /** IdentityDelivery API **/ 74 | 75 | @TableEndpoint(table = ChatDatabase.DELIVERED_IDENTITIES) 76 | public static class IdentityDeliveries { 77 | 78 | private static final String ENDPOINT = "identity_deliveries"; 79 | 80 | @ContentUri( 81 | path = ENDPOINT, 82 | type = "vnd.android.cursor.dir/list", 83 | defaultSort = IdentityDeliveryTable.peerRecipientId + " ASC") 84 | public static final Uri IDENTITY_DELIVERIES = buildUri(ENDPOINT); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/ChatDatabase.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import net.simonvt.schematic.annotation.Database; 4 | import net.simonvt.schematic.annotation.Table; 5 | 6 | /** 7 | * SQL Database definition. 8 | * 9 | * Created by davidbrodsky on 7/28/14. 10 | */ 11 | @Database(version = ChatDatabase.DATABASE_VERSION) 12 | public class ChatDatabase { 13 | 14 | public static final int DATABASE_VERSION = 1; 15 | 16 | /** Table Definition Reference Name SQL Tablename */ 17 | @Table(PeerTable.class) public static final String PEERS = "peers"; 18 | @Table(MessageTable.class) public static final String MESSAGES = "msgs"; 19 | @Table(MessageDeliveryTable.class) public static final String DELIVERED_MESSAGES = "m_dlvry"; 20 | @Table(IdentityDeliveryTable.class) public static final String DELIVERED_IDENTITIES = "p_dlvry"; 21 | } -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/CursorModel.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import android.database.Cursor; 4 | import android.support.annotation.NonNull; 5 | 6 | import java.io.Closeable; 7 | 8 | /** 9 | * Created by davidbrodsky on 10/20/14. 10 | */ 11 | public abstract class CursorModel implements Closeable{ 12 | 13 | protected Cursor mCursor; 14 | 15 | /** 16 | * Use this constructor if you intend to immediately access model data. 17 | * @param cursor A cursor that is already moved to the row corresponding to the desired model instance 18 | */ 19 | public CursorModel(@NonNull Cursor cursor) { 20 | mCursor = cursor; 21 | } 22 | 23 | public Cursor getCursor() { 24 | return mCursor; 25 | } 26 | 27 | @Override 28 | public void close() { 29 | if (mCursor != null) { 30 | mCursor.close(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/DataUtil.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Locale; 5 | 6 | /** 7 | * Utilities for converting between Java and Database friendly types 8 | * 9 | * Created by davidbrodsky on 10/13/14. 10 | */ 11 | public class DataUtil { 12 | 13 | public static SimpleDateFormat storedDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); 14 | 15 | final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); 16 | 17 | /** 18 | * When we query rows by a BLOB column we must 19 | * convert the BLOB to its String hex form 20 | * see: 21 | * http://www.sqlite.org/lang_expr.html#litvalue 22 | */ 23 | public static String bytesToHex(byte[] bytes) { 24 | char[] hexChars = new char[bytes.length * 2]; 25 | for ( int j = 0; j < bytes.length; j++ ) { 26 | int v = bytes[j] & 0xFF; 27 | hexChars[j * 2] = hexArray[v >>> 4]; 28 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 29 | } 30 | String rawHex = new String(hexChars); 31 | String blobLiteral = "X'" + rawHex + "'"; 32 | return blobLiteral; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/IdentityDeliveryTable.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import net.simonvt.schematic.annotation.AutoIncrement; 4 | import net.simonvt.schematic.annotation.DataType; 5 | import net.simonvt.schematic.annotation.NotNull; 6 | import net.simonvt.schematic.annotation.PrimaryKey; 7 | 8 | import static net.simonvt.schematic.annotation.DataType.Type.INTEGER; 9 | 10 | /** 11 | * Used to avoid sending a single identity to a particular client multiple times 12 | * 13 | * Created by davidbrodsky on 7/28/14. 14 | */ 15 | public interface IdentityDeliveryTable { 16 | 17 | /** SQL type Modifiers Reference Name SQL Column Name */ 18 | @DataType(INTEGER) @PrimaryKey @AutoIncrement String id = "_id"; 19 | @DataType(INTEGER) @NotNull String peerRecipientId = "pr_id"; 20 | @DataType(INTEGER) @NotNull String peerPayloadId = "pp_id"; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/Message.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import android.database.Cursor; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | 7 | import java.text.ParseException; 8 | import java.util.Date; 9 | 10 | import pro.dbro.ble.data.DataStore; 11 | import pro.dbro.ble.protocol.MessagePacket; 12 | 13 | /** 14 | * A thin model around a {@link android.database.Cursor} 15 | * that lazy-loads attributes as needed. As such, do 16 | * not to close the cursor fed to this class's constructor. 17 | * Instead call {@link #close} 18 | *

19 | * Created by davidbrodsky on 10/12/14. 20 | */ 21 | public class Message extends CursorModel { 22 | 23 | public Message(@NonNull Cursor cursor) { 24 | super(cursor); 25 | 26 | } 27 | 28 | public int getId() { 29 | return mCursor.getInt(mCursor.getColumnIndex(MessageTable.id)); 30 | } 31 | 32 | public String getBody() { 33 | return mCursor.getString(mCursor.getColumnIndex(MessageTable.body)); 34 | } 35 | 36 | public Date getAuthoredDate() { 37 | try { 38 | return DataUtil.storedDateFormatter.parse(mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate))); 39 | } catch (ParseException e) { 40 | e.printStackTrace(); 41 | return null; 42 | } 43 | } 44 | 45 | public byte[] getPublicKey(DataStore dataStore) { 46 | return getSender(dataStore).getIdentity().publicKey; 47 | } 48 | 49 | public byte[] getSignature() { 50 | return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.signature)); 51 | } 52 | 53 | public byte[] getReplySignature() { 54 | return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.replySig)); 55 | } 56 | 57 | public byte[] getRawPacket() { 58 | return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.rawPacket)); 59 | } 60 | 61 | @Nullable 62 | public Peer getSender(DataStore dataStore) { 63 | return dataStore.getPeerById(mCursor.getInt(mCursor.getColumnIndex(MessageTable.peerId))); 64 | } 65 | 66 | @Nullable 67 | public MessagePacket getProtocolMessage(DataStore dataStore) { 68 | return new MessagePacket( 69 | getSender(dataStore).getIdentity(), 70 | getSignature(), 71 | getReplySignature(), 72 | getBody(), 73 | getRawPacket(), 74 | getAuthoredDate()); 75 | 76 | } 77 | 78 | @Nullable 79 | public Date getRelativeReceivedDate() { 80 | try { 81 | return DataUtil.storedDateFormatter.parse( 82 | mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate))); 83 | } catch (ParseException e) { 84 | return null; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/MessageCollection.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import android.database.Cursor; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | 7 | 8 | /** 9 | * Created by davidbrodsky on 10/20/14. 10 | */ 11 | public class MessageCollection extends CursorModel { 12 | 13 | public MessageCollection(@NonNull Cursor cursor) { 14 | super(cursor); 15 | } 16 | 17 | @Nullable 18 | public Message getMessageAtPosition(int position) { 19 | boolean success = mCursor.move(position); 20 | if (success) 21 | return new Message(mCursor); 22 | return null; 23 | } 24 | 25 | public Cursor getCursor() { 26 | return mCursor; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/MessageDeliveryTable.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import net.simonvt.schematic.annotation.AutoIncrement; 4 | import net.simonvt.schematic.annotation.DataType; 5 | import net.simonvt.schematic.annotation.NotNull; 6 | import net.simonvt.schematic.annotation.PrimaryKey; 7 | 8 | import static net.simonvt.schematic.annotation.DataType.Type.INTEGER; 9 | 10 | /** 11 | * Used to avoid sending a single messages to a particular client multiple times 12 | * 13 | * Created by davidbrodsky on 7/28/14. 14 | */ 15 | public interface MessageDeliveryTable { 16 | 17 | /** SQL type Modifiers Reference Name SQL Column Name */ 18 | @DataType(INTEGER) @PrimaryKey @AutoIncrement String id = "_id"; 19 | @DataType(INTEGER) @NotNull String messageId = "m_id"; 20 | @DataType(INTEGER) @NotNull String peerId = "p_id"; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/MessageTable.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import net.simonvt.schematic.annotation.AutoIncrement; 4 | import net.simonvt.schematic.annotation.DataType; 5 | import net.simonvt.schematic.annotation.NotNull; 6 | import net.simonvt.schematic.annotation.PrimaryKey; 7 | 8 | import static net.simonvt.schematic.annotation.DataType.Type.BLOB; 9 | import static net.simonvt.schematic.annotation.DataType.Type.INTEGER; 10 | import static net.simonvt.schematic.annotation.DataType.Type.TEXT; 11 | 12 | /** 13 | * Created by davidbrodsky on 7/28/14. 14 | */ 15 | public interface MessageTable { 16 | 17 | /** SQL type Modifiers Reference Name SQL Column Name */ 18 | @DataType(INTEGER) @PrimaryKey @AutoIncrement String id = "_id"; 19 | @DataType(TEXT) @NotNull String body = "body"; 20 | @DataType(INTEGER) String peerId = "p_id"; 21 | @DataType(TEXT) String authoredDate = "author_date"; 22 | @DataType(TEXT) String receivedDate = "recv_date"; 23 | @DataType(BLOB) String signature = "sig"; 24 | @DataType(BLOB) String replySig = "r_sig"; 25 | @DataType(BLOB) String rawPacket = "pkt"; 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/Peer.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import android.database.Cursor; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | 7 | import java.text.ParseException; 8 | import java.util.Date; 9 | 10 | import pro.dbro.ble.protocol.IdentityPacket; 11 | import pro.dbro.ble.protocol.OwnedIdentityPacket; 12 | 13 | /** 14 | * Created by davidbrodsky on 10/12/14. 15 | */ 16 | public class Peer { 17 | 18 | private int mId; 19 | private byte[] mPublicKey; 20 | private byte[] mSecretKey; 21 | private String mAlias; 22 | private Date mLastSeen; 23 | 24 | private byte[] mRawPkt; 25 | 26 | 27 | public Peer(@NonNull Cursor cursor) { 28 | mId = cursor.getInt(cursor.getColumnIndex(PeerTable.id)); 29 | mPublicKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.pubKey)); 30 | mSecretKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.secKey)); 31 | mAlias = cursor.getString(cursor.getColumnIndex(PeerTable.alias)); 32 | mRawPkt = cursor.getBlob(cursor.getColumnIndex(PeerTable.rawPkt)); 33 | 34 | try { 35 | mLastSeen = DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(PeerTable.lastSeenDate))); 36 | } catch (ParseException e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | 41 | public int getId() { 42 | return mId; 43 | } 44 | 45 | public byte[] getPublicKey() { 46 | return mPublicKey; 47 | } 48 | 49 | public String getAlias() { 50 | return mAlias; 51 | } 52 | 53 | @Nullable 54 | public Date getLastDateSeen() { 55 | return mLastSeen; 56 | } 57 | /** 58 | * @return whether this peer represents the application user. 59 | * e.g: Do we have a secret key 60 | */ 61 | public boolean isLocalPeer() { 62 | return mSecretKey != null && mSecretKey.length > 0; 63 | } 64 | 65 | /** 66 | * @return a {@link pro.dbro.ble.protocol.OwnedIdentityPacket} for this peer, 67 | * or an {@link pro.dbro.ble.protocol.IdentityPacket} if this peer is not a user-owned peer. 68 | *

69 | * see {@link #isLocalPeer()} 70 | */ 71 | public IdentityPacket getIdentity() { 72 | if (!isLocalPeer()) { 73 | return new IdentityPacket(mPublicKey, mAlias, mLastSeen, mRawPkt); 74 | } else { 75 | return new OwnedIdentityPacket(mSecretKey, mPublicKey, mAlias, mRawPkt); 76 | } 77 | } 78 | 79 | @Override 80 | public boolean equals(Object obj) { 81 | 82 | if(obj == this) return true; 83 | if(obj == null) return false; 84 | 85 | if (getClass().equals(obj.getClass())) 86 | { 87 | final Peer other = (Peer) obj; 88 | 89 | return mId == other.mId; 90 | } 91 | 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/data/model/PeerTable.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.data.model; 2 | 3 | import net.simonvt.schematic.annotation.AutoIncrement; 4 | import net.simonvt.schematic.annotation.DataType; 5 | import net.simonvt.schematic.annotation.NotNull; 6 | import net.simonvt.schematic.annotation.PrimaryKey; 7 | 8 | import static net.simonvt.schematic.annotation.DataType.Type.BLOB; 9 | import static net.simonvt.schematic.annotation.DataType.Type.INTEGER; 10 | import static net.simonvt.schematic.annotation.DataType.Type.TEXT; 11 | 12 | /** 13 | * Created by davidbrodsky on 7/28/14. 14 | */ 15 | public interface PeerTable { 16 | 17 | /** SQL type Modifiers Reference Name SQL Column Name */ 18 | @DataType(INTEGER) @PrimaryKey @AutoIncrement String id = "_id"; 19 | @DataType(TEXT) String alias = "alias"; 20 | @DataType(TEXT) @NotNull String lastSeenDate = "last_seen"; 21 | @DataType(BLOB) @NotNull String pubKey = "pk"; 22 | @DataType(BLOB) String secKey = "sk"; 23 | @DataType(BLOB) String rawPkt = "pkt"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/protocol/IdentityPacket.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.protocol; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * An identity for a remote peer 10 | * Created by davidbrodsky on 10/13/14. 11 | */ 12 | public class IdentityPacket { 13 | public static final byte TYPE = 0x01; 14 | 15 | public final byte[] publicKey; 16 | public final Date dateSeen; 17 | public final String alias; 18 | public final byte[] rawPacket; 19 | 20 | public IdentityPacket(@NonNull final byte[] publicKey, @Nullable String alias, @NonNull Date dateSeen, 21 | @NonNull final byte[] rawPacket) { 22 | // dateSeen is allowed null because it's meaningless for OwnedIdentities 23 | this.publicKey = publicKey; 24 | this.alias = alias == null ? null : alias.trim(); 25 | this.dateSeen = dateSeen; 26 | this.rawPacket = rawPacket; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/protocol/MessagePacket.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.protocol; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by davidbrodsky on 10/15/14. 9 | */ 10 | public class MessagePacket { 11 | public static final byte TYPE = 0x02; 12 | 13 | final public IdentityPacket sender; 14 | final public String body; 15 | final public Date authoredDate; 16 | final public byte[] signature; 17 | final public byte[] replySig; 18 | final public byte[] rawPacket; 19 | 20 | /** Incoming */ 21 | public MessagePacket(@NonNull final byte[] publicKey, 22 | @NonNull byte[] signature, 23 | @NonNull byte[] replySig, 24 | @NonNull Date authoredDate, 25 | @NonNull String body, 26 | @NonNull byte[] rawPacket) { 27 | 28 | this.body = body; 29 | this.signature = signature; 30 | this.replySig = replySig; 31 | this.rawPacket = rawPacket; 32 | this.authoredDate = authoredDate; 33 | sender = new IdentityPacket(publicKey, null, null, null); // We don't have the sender's full identity response 34 | } 35 | 36 | public static MessagePacket attachIdentityToMessage(@NonNull MessagePacket message, @NonNull IdentityPacket identity) { 37 | return new MessagePacket(identity, message.signature, message.replySig, message.body, message.rawPacket, message.authoredDate); 38 | } 39 | 40 | /** Outgoing */ 41 | public MessagePacket(@NonNull IdentityPacket sender, 42 | @NonNull byte[] signature, 43 | @NonNull byte[] replySig, 44 | @NonNull String body, 45 | @NonNull byte[] rawPacket, 46 | @NonNull Date authoredDate) { 47 | 48 | this.body = body.trim(); 49 | this.signature = signature; 50 | this.replySig = replySig; 51 | this.rawPacket = rawPacket; 52 | this.authoredDate = authoredDate; 53 | this.sender = sender; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/protocol/NoDataPacket.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.protocol; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by davidbrodsky on 10/15/14. 9 | * 10 | */ 11 | public class NoDataPacket { 12 | public static final byte TYPE = 0x03; 13 | 14 | final public byte[] publicKey; 15 | final public Date authoredDate; 16 | final public byte[] signature; 17 | final public byte[] rawPacket; 18 | 19 | public NoDataPacket(@NonNull final byte[] publicKey, 20 | @NonNull Date authoredDate, 21 | @NonNull byte[] signature, 22 | @NonNull byte[] rawPacket) { 23 | 24 | this.publicKey = publicKey; 25 | this.signature = signature; 26 | this.rawPacket = rawPacket; 27 | this.authoredDate = authoredDate; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/protocol/OwnedIdentityPacket.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.protocol; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | /** 6 | * An Identity for the local peer 7 | * Created by davidbrodsky on 10/13/14. 8 | */ 9 | public class OwnedIdentityPacket extends IdentityPacket { 10 | 11 | public final byte[] secretKey; 12 | 13 | public OwnedIdentityPacket(@NonNull final byte[] secretKey, @NonNull final byte[] publicKey, 14 | @NonNull String alias, byte[] rawPacket) { 15 | super(publicKey, alias, null, rawPacket); 16 | this.secretKey = secretKey; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/protocol/Protocol.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.protocol; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | /** 7 | * Created by davidbrodsky on 10/20/14. 8 | */ 9 | public interface Protocol { 10 | 11 | /** Outgoing 12 | * 13 | * Serialize Protocol Objects to raw transmission data 14 | **/ 15 | 16 | // TODO Decide on a consistent API here 17 | public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity); 18 | 19 | public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body); 20 | 21 | public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity); 22 | 23 | /** Incoming 24 | * 25 | * Deserialize raw transmission data into Protocol Objects 26 | */ 27 | 28 | public IdentityPacket deserializeIdentity(@NonNull byte[] identity); 29 | 30 | /** Deserialize a message where the author identity is known */ 31 | public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @Nullable IdentityPacket identity); 32 | 33 | /** Deserialize a message where the author identity is not known */ 34 | public MessagePacket deserializeMessage(@NonNull byte[] message); 35 | 36 | public byte getPacketType(@NonNull byte[] message); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/Notification.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui; 2 | 3 | import android.app.NotificationManager; 4 | import android.app.PendingIntent; 5 | import android.app.TaskStackBuilder; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.graphics.Bitmap; 9 | import android.graphics.Canvas; 10 | import android.support.annotation.NonNull; 11 | import android.support.annotation.Nullable; 12 | import android.support.v4.app.NotificationCompat; 13 | import android.view.View; 14 | 15 | import java.util.ArrayList; 16 | 17 | import im.delight.android.identicons.SymmetricIdenticon; 18 | import pro.dbro.ble.R; 19 | import pro.dbro.ble.data.model.DataUtil; 20 | import pro.dbro.ble.data.model.Message; 21 | import pro.dbro.ble.data.model.Peer; 22 | import pro.dbro.ble.ui.activities.MainActivity; 23 | 24 | /** 25 | * Created by davidbrodsky on 11/14/14. 26 | */ 27 | public class Notification { 28 | 29 | /** Notification Ids */ 30 | private static final int MESSAGE_NOTIFICATION_ID = 1; 31 | private static final int PEER_AVAILABLE_NOTIFICATION_ID = 2; 32 | 33 | private static final int MAX_MESSAGES_TO_SHOW = 6; 34 | 35 | private static final ArrayList sNotificationInboxItems = new ArrayList<>(MAX_MESSAGES_TO_SHOW + 1); 36 | 37 | // 38 | 39 | /** 40 | * Display a notification representing peer being available, or remove any indicating such 41 | * if isAvailable is false. 42 | * 43 | * Does not call peer.close() 44 | */ 45 | public static void displayPeerAvailableNotification(@NonNull Context context, @NonNull Peer peer, boolean isAvailable) { 46 | NotificationManager mNotificationManager = 47 | (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 48 | 49 | if (!isAvailable) { 50 | mNotificationManager.cancel(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID); 51 | return; 52 | } 53 | if (peer.getAlias() == null) return; // TODO : Notify of peers without alias? 54 | 55 | String title = String.format("%s is nearby", peer.getAlias()); 56 | 57 | Intent resultIntent = new Intent(context, MainActivity.class); 58 | 59 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 60 | builder.setSmallIcon(R.mipmap.ic_launcher); 61 | builder.setContentTitle(title); 62 | builder.setContentIntent(makePendingIntent(context, resultIntent)); 63 | builder.setContentText(context.getString(R.string.notification_touch_to_chat)); 64 | 65 | mNotificationManager.notify(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID, builder.build()); 66 | } 67 | 68 | /** 69 | * Display a notification representing a new received message. Multiple calls to this method are displayed as a single 70 | * notification, showing a preview of the last MAX_MESSAGES_TO_SHOW messages. 71 | * 72 | * Does not call message.close() or sender.close() 73 | */ 74 | public static void displayMessageNotification(@NonNull Context context, @NonNull Message message, @Nullable Peer sender) { 75 | StringBuilder nBuilder = new StringBuilder(); 76 | if (sender != null && sender.getAlias() != null) { 77 | nBuilder.append(sender.getAlias()); 78 | nBuilder.append(": "); 79 | } 80 | nBuilder.append(message.getBody().length() > 80 ? 81 | message.getBody().substring(0, 80) + "..." : 82 | message.getBody()); 83 | sNotificationInboxItems.add(nBuilder.toString()); 84 | if (sNotificationInboxItems.size() > MAX_MESSAGES_TO_SHOW) sNotificationInboxItems.remove(sNotificationInboxItems.size()-1); 85 | 86 | Intent resultIntent = new Intent(context, MainActivity.class); 87 | 88 | NotificationCompat.InboxStyle inboxStyle = 89 | new NotificationCompat.InboxStyle(); 90 | inboxStyle.setBigContentTitle(context.getString(R.string.notification_new_messages)); 91 | 92 | for (String inboxItem : sNotificationInboxItems) { 93 | inboxStyle.addLine(inboxItem); 94 | } 95 | 96 | SymmetricIdenticon identicon = new SymmetricIdenticon(context); 97 | identicon.show(new String(sender.getPublicKey())); 98 | 99 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 100 | builder.setContentTitle(context.getString(R.string.notification_new_messages)); 101 | builder.setLargeIcon(loadBitmapFromView(identicon, 640, 480)); 102 | builder.setSmallIcon(R.mipmap.ic_launcher); 103 | builder.setContentIntent(makePendingIntent(context, resultIntent)); 104 | builder.setStyle(inboxStyle); 105 | builder.setContentText(sNotificationInboxItems.get(0)); 106 | builder.setAutoCancel(true); 107 | builder.setCategory(NotificationCompat.CATEGORY_MESSAGE); 108 | builder.setVibrate(new long[] { 500, 500, 500, 500}); 109 | 110 | NotificationManager mNotificationManager = 111 | (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 112 | 113 | mNotificationManager.notify(MESSAGE_NOTIFICATION_ID, builder.build()); 114 | } 115 | 116 | // 117 | 118 | // 119 | 120 | private static PendingIntent makePendingIntent(@NonNull Context context, @NonNull Intent resultIntent) { 121 | TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); 122 | // Adds the back stack 123 | stackBuilder.addParentStack(MainActivity.class); 124 | // Adds the Intent to the top of the stack 125 | stackBuilder.addNextIntent(resultIntent); 126 | // Gets a PendingIntent containing the entire back stack 127 | return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); 128 | } 129 | 130 | public static Bitmap loadBitmapFromView(View v, int width, int height) { 131 | 132 | int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, 133 | View.MeasureSpec.EXACTLY); 134 | int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, 135 | View.MeasureSpec.EXACTLY); 136 | v.measure(measuredWidth, measuredHeight); 137 | v.layout(0, 0, v.getMeasuredWidth(),v.getMeasuredHeight()); 138 | 139 | Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 140 | Canvas c = new Canvas(b); 141 | v.draw(c); 142 | return b; 143 | } 144 | 145 | // 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/activities/LogConsumer.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.activities; 2 | 3 | /** 4 | * Created by davidbrodsky on 10/11/14. 5 | */ 6 | public interface LogConsumer { 7 | public void onLogEvent(String event); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/activities/Util.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.activities; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.support.annotation.NonNull; 7 | import android.view.KeyEvent; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.widget.EditText; 11 | import android.widget.TextView; 12 | 13 | import pro.dbro.ble.ChatClient; 14 | import pro.dbro.ble.R; 15 | 16 | /** 17 | * Created by davidbrodsky on 10/13/14. 18 | */ 19 | public class Util { 20 | 21 | public static void showWelcomeDialog(@NonNull final ChatClient app, @NonNull final Context context, DialogInterface.OnDismissListener dismissListener) { 22 | View dialogView = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) 23 | .inflate(R.layout.dialog_welcome, null); 24 | final EditText aliasEntry = ((EditText) dialogView.findViewById(R.id.aliasEntry)); 25 | 26 | AlertDialog.Builder builder = new AlertDialog.Builder(context); 27 | final AlertDialog dialog = builder.setTitle(context.getString(R.string.dialog_welcome_greeting)) 28 | .setView(dialogView) 29 | .setPositiveButton(context.getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { 30 | @Override 31 | public void onClick(DialogInterface dialogInterface, int i) { 32 | app.createPrimaryIdentity(aliasEntry.getText().toString()); 33 | } 34 | }) 35 | .setOnDismissListener(dismissListener) 36 | .show(); 37 | aliasEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() { 38 | @Override 39 | public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { 40 | app.createPrimaryIdentity(textView.getText().toString()); 41 | dialog.dismiss(); 42 | return false; 43 | } 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/adapter/CursorFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pro.dbro.ble.ui.adapter; 18 | 19 | import android.database.Cursor; 20 | import android.widget.Filter; 21 | 22 | /** 23 | * The CursorFilter delegates most of the work to the 24 | * {@link android.widget.CursorAdapter}. Subclasses should override these 25 | * delegate methods to run the queries and convert the results into String 26 | * that can be used by auto-completion widgets. 27 | */ 28 | class CursorFilter extends Filter { 29 | 30 | CursorFilterClient mClient; 31 | 32 | interface CursorFilterClient { 33 | CharSequence convertToString(Cursor cursor); 34 | Cursor runQueryOnBackgroundThread(CharSequence constraint); 35 | Cursor getCursor(); 36 | void changeCursor(Cursor cursor); 37 | } 38 | 39 | CursorFilter(CursorFilterClient client) { 40 | mClient = client; 41 | } 42 | 43 | @Override 44 | public CharSequence convertResultToString(Object resultValue) { 45 | return mClient.convertToString((Cursor) resultValue); 46 | } 47 | 48 | @Override 49 | protected FilterResults performFiltering(CharSequence constraint) { 50 | Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); 51 | 52 | FilterResults results = new FilterResults(); 53 | if (cursor != null) { 54 | results.count = cursor.getCount(); 55 | results.values = cursor; 56 | } else { 57 | results.count = 0; 58 | results.values = null; 59 | } 60 | return results; 61 | } 62 | 63 | @Override 64 | protected void publishResults(CharSequence constraint, FilterResults results) { 65 | Cursor oldCursor = mClient.getCursor(); 66 | 67 | if (results.values != null && results.values != oldCursor) { 68 | mClient.changeCursor((Cursor) results.values); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/adapter/MessageAdapter.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.text.format.DateUtils; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.TextView; 14 | 15 | import java.text.ParseException; 16 | import java.util.UUID; 17 | 18 | import im.delight.android.identicons.SymmetricIdenticon; 19 | import pro.dbro.ble.R; 20 | import pro.dbro.ble.data.DataStore; 21 | import pro.dbro.ble.data.model.DataUtil; 22 | import pro.dbro.ble.data.model.MessageTable; 23 | import pro.dbro.ble.data.model.Peer; 24 | 25 | /** 26 | * Created by davidbrodsky on 10/19/14. 27 | */ 28 | public class MessageAdapter extends RecyclerViewCursorAdapter { 29 | public static final String TAG = "MessageAdapter"; 30 | 31 | public interface MessageSelectedListener { 32 | void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId); 33 | } 34 | 35 | private DataStore mDataStore; 36 | private RecyclerView mHost; 37 | private MessageSelectedListener mListener; 38 | 39 | public static class ViewHolder extends RecyclerView.ViewHolder { 40 | public View container; 41 | public TextView senderView; 42 | public TextView messageView; 43 | public TextView authoredView; 44 | public SymmetricIdenticon identicon; 45 | public Peer peer; 46 | 47 | 48 | public ViewHolder(View v) { 49 | super(v); 50 | container = v; 51 | senderView = (TextView) v.findViewById(R.id.sender); 52 | messageView = (TextView) v.findViewById(R.id.messageBody); 53 | authoredView = (TextView) v.findViewById(R.id.authoredDate); 54 | identicon = (SymmetricIdenticon) v.findViewById(R.id.identicon); 55 | 56 | } 57 | } 58 | 59 | /** 60 | * Recommended constructor. 61 | * 62 | * @param context The context 63 | * @param dataStore The data backend 64 | * @param fromPeer A Peer to show messages from, or null to show all messages 65 | * @param flags Flags used to determine the behavior of the adapter; 66 | * Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}. 67 | */ 68 | public MessageAdapter(@NonNull Context context, 69 | @Nullable Peer fromPeer, 70 | @NonNull DataStore dataStore, 71 | @Nullable MessageSelectedListener listener, 72 | int flags) { 73 | super(context, 74 | fromPeer == null ? dataStore.getRecentMessages().getCursor() : 75 | dataStore.getRecentMessagesByPeer(fromPeer).getCursor(), flags); 76 | mDataStore = dataStore; 77 | mListener = listener; 78 | } 79 | 80 | @Override 81 | public void onAttachedToRecyclerView(RecyclerView recyclerView) { 82 | mHost = recyclerView; 83 | } 84 | 85 | @Override 86 | public void onBindViewHolder(ViewHolder holder, Cursor cursor) { 87 | holder.container.setTag(R.id.view_tag_msg_id, cursor.getInt(cursor.getColumnIndex(MessageTable.id))); 88 | 89 | if (holder.peer == null) // TODO : Should do this lookup on a background thread 90 | holder.peer = mDataStore.getPeerById(cursor.getInt(cursor.getColumnIndex(MessageTable.peerId))); 91 | 92 | if (holder.peer != null) { 93 | holder.container.setTag(R.id.view_tag_peer_id, holder.peer.getId()); 94 | holder.senderView.setText(holder.peer.getAlias()); 95 | holder.identicon.show(new String(holder.peer.getPublicKey())); 96 | } else { 97 | holder.senderView.setText("?"); 98 | holder.identicon.show(UUID.randomUUID()); 99 | } 100 | holder.messageView.setText(cursor.getString(cursor.getColumnIndex(MessageTable.body))); 101 | try { 102 | holder.authoredView.setText(DateUtils.getRelativeTimeSpanString( 103 | DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(MessageTable.authoredDate))).getTime())); 104 | } catch (ParseException e) { 105 | holder.authoredView.setText(""); 106 | e.printStackTrace(); 107 | } 108 | } 109 | 110 | @Override 111 | protected void onContentChanged() { 112 | Log.i(TAG, "onContentChanged"); 113 | changeCursor(mDataStore.getRecentMessages().getCursor()); 114 | mHost.smoothScrollToPosition(0); 115 | } 116 | 117 | @Override 118 | public ViewHolder onCreateViewHolder(ViewGroup parent, int i) { 119 | View v = LayoutInflater.from(parent.getContext()) 120 | .inflate(R.layout.message_item, parent, false); 121 | 122 | v.setOnClickListener(new View.OnClickListener() { 123 | @Override 124 | public void onClick(View v) { 125 | if (mListener != null) 126 | mListener.onMessageSelected(v.findViewById(R.id.identicon), 127 | v.findViewById(R.id.sender), 128 | (Integer) v.getTag(R.id.view_tag_msg_id), 129 | (Integer) v.getTag(R.id.view_tag_peer_id)); 130 | } 131 | }); 132 | // set the view's size, margins, paddings and layout parameters 133 | return new ViewHolder(v); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/adapter/PeerAdapter.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import java.util.ArrayList; 11 | 12 | import im.delight.android.identicons.SymmetricIdenticon; 13 | import pro.dbro.ble.R; 14 | import pro.dbro.ble.data.DataStore; 15 | import pro.dbro.ble.data.model.Message; 16 | import pro.dbro.ble.data.model.Peer; 17 | 18 | /** 19 | * Created by davidbrodsky on 10/12/14. 20 | */ 21 | public class PeerAdapter extends RecyclerView.Adapter { 22 | private Context mContext; 23 | private ArrayList mPeers; 24 | 25 | // Provide a reference to the type of views that you are using 26 | // (custom viewholder) 27 | public static class ViewHolder extends RecyclerView.ViewHolder { 28 | public TextView mTextView; 29 | SymmetricIdenticon mIdenticon; 30 | 31 | public ViewHolder(View v) { 32 | super(v); 33 | mTextView = (TextView) v.findViewById(R.id.username); 34 | mIdenticon = (SymmetricIdenticon) v.findViewById(R.id.identicon); 35 | } 36 | } 37 | 38 | // Provide a suitable constructor (depends on the kind of dataset) 39 | public PeerAdapter(Context context, ArrayList peers) { 40 | mPeers = peers; 41 | mContext = context; 42 | } 43 | 44 | // Create new views (invoked by the layout manager) 45 | @Override 46 | public PeerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, 47 | int viewType) { 48 | // create a new view 49 | View v = LayoutInflater.from(parent.getContext()) 50 | .inflate(R.layout.peer_item, parent, false); 51 | // set the view's size, margins, paddings and layout parameters 52 | ViewHolder vh = new ViewHolder(v); 53 | return vh; 54 | } 55 | 56 | // Replace the contents of a view (invoked by the layout manager) 57 | @Override 58 | public void onBindViewHolder(ViewHolder holder, int position) { 59 | // - get element from your dataset at this position 60 | // - replace the contents of the view with that element 61 | Peer peer = mPeers.get(position); 62 | holder.mTextView.setText(peer.getAlias()); 63 | holder.mIdenticon.show(new String(peer.getPublicKey())); 64 | } 65 | 66 | // Return the size of your dataset (invoked by the layout manager) 67 | @Override 68 | public int getItemCount() { 69 | return mPeers.size(); 70 | } 71 | 72 | public void notifyPeerAdded(Peer peer) { 73 | mPeers.add(peer); 74 | notifyItemInserted(mPeers.size()-1); 75 | } 76 | 77 | public void notifyPeerRemoved(Peer peer) { 78 | int idx = mPeers.indexOf(peer); 79 | if (idx != -1) { 80 | mPeers.remove(idx); 81 | notifyItemRemoved(idx); 82 | } 83 | } 84 | 85 | public void clearPeers() { 86 | mPeers.clear(); 87 | notifyDataSetChanged(); 88 | } 89 | 90 | public void notifyMessageReceived(DataStore manager, Message message) { 91 | Peer peer = message.getSender(manager); 92 | if (peer != null) { 93 | int oldIdx = mPeers.indexOf(peer); 94 | if (oldIdx != -1 ) { 95 | mPeers.remove(peer); 96 | mPeers.add(0, peer); 97 | notifyItemMoved(oldIdx, 0); 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/adapter/RecyclerViewCursorAdapter.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.adapter; 2 | 3 | /* 4 | * Copyright (C) 2013 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | /* 20 | * Copyright (C) 2014 flzyup@ligux.com 21 | * 22 | * Licensed under the Apache License, Version 2.0 (the "License"); 23 | * you may not use this file except in compliance with the License. 24 | * You may obtain a copy of the License at 25 | * 26 | * http://www.apache.org/licenses/LICENSE-2.0 27 | * 28 | * Unless required by applicable law or agreed to in writing, software 29 | * distributed under the License is distributed on an "AS IS" BASIS, 30 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | * See the License for the specific language governing permissions and 32 | * limitations under the License. 33 | * 34 | */ 35 | import android.content.Context; 36 | import android.database.ContentObserver; 37 | import android.database.Cursor; 38 | import android.database.DataSetObserver; 39 | import android.os.Handler; 40 | import android.support.v7.widget.RecyclerView; 41 | import android.widget.Filter; 42 | import android.widget.FilterQueryProvider; 43 | import android.widget.Filterable; 44 | 45 | /** 46 | * Version 1.0 47 | *

48 | * Date: 2014-07-07 19:53 49 | * Author: flzyup@ligux.com 50 | *

51 | * Copyright © 2009-2014 LiGux.com. 52 | */ 53 | public abstract class RecyclerViewCursorAdapter extends RecyclerView.Adapter implements Filterable, 54 | CursorFilter.CursorFilterClient { 55 | 56 | /** 57 | * Call when bind view with the cursor 58 | * 59 | * @param holder 60 | * @param cursor 61 | */ 62 | public abstract void onBindViewHolder(VH holder, Cursor cursor); 63 | 64 | /** 65 | * This field should be made private, so it is hidden from the SDK. 66 | * {@hide} 67 | */ 68 | protected boolean mDataValid; 69 | 70 | /** 71 | * The current cursor 72 | */ 73 | protected Cursor mCursor; 74 | 75 | /** 76 | * This field should be made private, so it is hidden from the SDK. 77 | * {@hide} 78 | */ 79 | protected Context mContext; 80 | 81 | /** 82 | * The row id column 83 | */ 84 | protected int mRowIDColumn; 85 | 86 | /** 87 | * This field should be made private, so it is hidden from the SDK. 88 | * {@hide} 89 | */ 90 | protected ChangeObserver mChangeObserver; 91 | /** 92 | * This field should be made private, so it is hidden from the SDK. 93 | * {@hide} 94 | */ 95 | protected DataSetObserver mDataSetObserver; 96 | 97 | /** 98 | * This field should be made private, so it is hidden from the SDK. 99 | * {@hide} 100 | */ 101 | protected CursorFilter mCursorFilter; 102 | 103 | /** 104 | * This field should be made private, so it is hidden from the SDK. 105 | * {@hide} 106 | */ 107 | protected FilterQueryProvider mFilterQueryProvider; 108 | 109 | /** 110 | * If set the adapter will register a content observer on the cursor and will call 111 | * {@link #onContentChanged()} when a notification comes in. Be careful when 112 | * using this flag: you will need to unset the current Cursor from the adapter 113 | * to avoid leaks due to its registered observers. This flag is not needed 114 | * when using a CursorAdapter with a 115 | * {@link android.content.CursorLoader}. 116 | */ 117 | public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; 118 | 119 | /** 120 | * Recommended constructor. 121 | * 122 | * @param c The cursor from which to get the data. 123 | * @param context The context 124 | * @param flags Flags used to determine the behavior of the adapter; 125 | * Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}. 126 | */ 127 | public RecyclerViewCursorAdapter(Context context, Cursor c, int flags) { 128 | init(context, c, flags); 129 | } 130 | 131 | void init(Context context, Cursor c, int flags) { 132 | 133 | boolean cursorPresent = c != null; 134 | mCursor = c; 135 | mDataValid = cursorPresent; 136 | mContext = context; 137 | mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; 138 | if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { 139 | mChangeObserver = new ChangeObserver(); 140 | mDataSetObserver = new MyDataSetObserver(); 141 | } else { 142 | mChangeObserver = null; 143 | mDataSetObserver = null; 144 | } 145 | 146 | if (cursorPresent) { 147 | if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); 148 | if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); 149 | } 150 | setHasStableIds(true); 151 | } 152 | 153 | /** 154 | * Returns the cursor. 155 | * 156 | * @return the cursor. 157 | */ 158 | @Override 159 | public Cursor getCursor() { 160 | return mCursor; 161 | } 162 | 163 | /** 164 | * @see android.support.v7.widget.RecyclerView.Adapter#getItemCount() 165 | */ 166 | @Override 167 | public int getItemCount() { 168 | if (mDataValid && mCursor != null) { 169 | return mCursor.getCount(); 170 | } else { 171 | return 0; 172 | } 173 | } 174 | 175 | /** 176 | * @param position Adapter position to query 177 | * @return 178 | * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int) 179 | */ 180 | @Override 181 | public long getItemId(int position) { 182 | if (mDataValid && mCursor != null) { 183 | if (mCursor.moveToPosition(position)) { 184 | return mCursor.getLong(mRowIDColumn); 185 | } else { 186 | return 0; 187 | } 188 | } else { 189 | return 0; 190 | } 191 | } 192 | 193 | @Override 194 | public void onBindViewHolder(VH holder, int position) { 195 | if (!mDataValid) { 196 | throw new IllegalStateException("this should only be called when the cursor is valid"); 197 | } 198 | if (!mCursor.moveToPosition(position)) { 199 | throw new IllegalStateException("couldn't move cursor to position " + position); 200 | } 201 | onBindViewHolder(holder, mCursor); 202 | } 203 | 204 | /** 205 | * Change the underlying cursor to a new cursor. If there is an existing cursor it will be 206 | * closed. 207 | * 208 | * @param cursor The new cursor to be used 209 | */ 210 | public void changeCursor(Cursor cursor) { 211 | Cursor old = swapCursor(cursor); 212 | if (old != null) { 213 | old.close(); 214 | } 215 | } 216 | 217 | /** 218 | * Swap in a new Cursor, returning the old Cursor. Unlike 219 | * {@link #changeCursor(Cursor)}, the returned old Cursor is not 220 | * closed. 221 | * 222 | * @param newCursor The new cursor to be used. 223 | * @return Returns the previously set Cursor, or null if there wasa not one. 224 | * If the given new Cursor is the same instance is the previously set 225 | * Cursor, null is also returned. 226 | */ 227 | public Cursor swapCursor(Cursor newCursor) { 228 | if (newCursor == mCursor) { 229 | return null; 230 | } 231 | Cursor oldCursor = mCursor; 232 | if (oldCursor != null) { 233 | if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); 234 | if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); 235 | } 236 | mCursor = newCursor; 237 | if (newCursor != null) { 238 | if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); 239 | if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); 240 | mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); 241 | mDataValid = true; 242 | // notify the observers about the new cursor 243 | notifyDataSetChanged(); 244 | } else { 245 | mRowIDColumn = -1; 246 | mDataValid = false; 247 | // notify the observers about the lack of a data set 248 | notifyDataSetChanged(); 249 | // notifyDataSetInvalidated(); 250 | } 251 | return oldCursor; 252 | } 253 | 254 | /** 255 | *

Converts the cursor into a CharSequence. Subclasses should override this 256 | * method to convert their results. The default implementation returns an 257 | * empty String for null values or the default String representation of 258 | * the value.

259 | * 260 | * @param cursor the cursor to convert to a CharSequence 261 | * @return a CharSequence representing the value 262 | */ 263 | public CharSequence convertToString(Cursor cursor) { 264 | return cursor == null ? "" : cursor.toString(); 265 | } 266 | 267 | /** 268 | * Runs a query with the specified constraint. This query is requested 269 | * by the filter attached to this adapter. 270 | *

271 | * The query is provided by a 272 | * {@link android.widget.FilterQueryProvider}. 273 | * If no provider is specified, the current cursor is not filtered and returned. 274 | *

275 | * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} 276 | * and the previous cursor is closed. 277 | *

278 | * This method is always executed on a background thread, not on the 279 | * application's main thread (or UI thread.) 280 | *

281 | * Contract: when constraint is null or empty, the original results, 282 | * prior to any filtering, must be returned. 283 | * 284 | * @param constraint the constraint with which the query must be filtered 285 | * @return a Cursor representing the results of the new query 286 | * @see #getFilter() 287 | * @see #getFilterQueryProvider() 288 | * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 289 | */ 290 | public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 291 | if (mFilterQueryProvider != null) { 292 | return mFilterQueryProvider.runQuery(constraint); 293 | } 294 | 295 | return mCursor; 296 | } 297 | 298 | public Filter getFilter() { 299 | if (mCursorFilter == null) { 300 | mCursorFilter = new CursorFilter(this); 301 | } 302 | return mCursorFilter; 303 | } 304 | 305 | /** 306 | * Returns the query filter provider used for filtering. When the 307 | * provider is null, no filtering occurs. 308 | * 309 | * @return the current filter query provider or null if it does not exist 310 | * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 311 | * @see #runQueryOnBackgroundThread(CharSequence) 312 | */ 313 | public FilterQueryProvider getFilterQueryProvider() { 314 | return mFilterQueryProvider; 315 | } 316 | 317 | /** 318 | * Sets the query filter provider used to filter the current Cursor. 319 | * The provider's 320 | * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} 321 | * method is invoked when filtering is requested by a client of 322 | * this adapter. 323 | * 324 | * @param filterQueryProvider the filter query provider or null to remove it 325 | * @see #getFilterQueryProvider() 326 | * @see #runQueryOnBackgroundThread(CharSequence) 327 | */ 328 | public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { 329 | mFilterQueryProvider = filterQueryProvider; 330 | } 331 | 332 | /** 333 | * Called when the {@link ContentObserver} on the cursor receives a change notification. 334 | * The default implementation provides the auto-requery logic, but may be overridden by 335 | * sub classes. 336 | * 337 | * @see ContentObserver#onChange(boolean) 338 | */ 339 | protected abstract void onContentChanged(); 340 | 341 | private class ChangeObserver extends ContentObserver { 342 | public ChangeObserver() { 343 | super(new Handler()); 344 | } 345 | 346 | @Override 347 | public boolean deliverSelfNotifications() { 348 | return true; 349 | } 350 | 351 | @Override 352 | public void onChange(boolean selfChange) { 353 | onContentChanged(); 354 | } 355 | } 356 | 357 | private class MyDataSetObserver extends DataSetObserver { 358 | @Override 359 | public void onChanged() { 360 | mDataValid = true; 361 | notifyDataSetChanged(); 362 | } 363 | 364 | @Override 365 | public void onInvalidated() { 366 | mDataValid = false; 367 | notifyDataSetChanged(); 368 | // notifyDataSetInvalidated(); 369 | } 370 | } 371 | } -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/adapter/StatusArrayAdapter.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.util.TypedValue; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ArrayAdapter; 10 | import android.widget.TextView; 11 | 12 | import java.util.ArrayList; 13 | 14 | import pro.dbro.ble.R; 15 | import timber.log.Timber; 16 | 17 | /** 18 | * Created by davidbrodsky on 4/20/15. 19 | */ 20 | public class StatusArrayAdapter extends ArrayAdapter { 21 | 22 | public StatusArrayAdapter(Context context, ArrayList statuses) { 23 | super(context, android.R.layout.simple_spinner_dropdown_item, statuses); 24 | } 25 | 26 | @Override 27 | public View getDropDownView(int position, View convertView, ViewGroup parent) { 28 | return getCustomView(position, convertView, parent); 29 | } 30 | 31 | @Override 32 | public View getView(int position, View convertView, ViewGroup parent) { 33 | return getCustomView(position, convertView, parent); 34 | } 35 | 36 | public View getCustomView(int position, View convertView, ViewGroup parent) { 37 | 38 | Context context = parent.getContext(); 39 | 40 | // Get the data item for this position 41 | String status = getItem(position); 42 | // Check if an existing view is being reused, otherwise inflate the view 43 | if (convertView == null) { 44 | convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_spinner_dropdown_item, parent, false); 45 | ((TextView) convertView).setCompoundDrawablePadding((int) dipToPixels(context, 8)); 46 | } 47 | 48 | TextView statusLabel = (TextView) convertView; 49 | statusLabel.setText(status); 50 | 51 | String[] choices = context.getResources().getStringArray(R.array.status_options); 52 | if (status.equals(choices[0])) { // Always online 53 | statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_always_online), null, null, null); 54 | } 55 | else if (status.equals(choices[1])) { // Online when using app 56 | statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_online_in_foreground), null, null, null); 57 | } else if (status.equals(choices[2])) { // Offline 58 | statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_offline), null, null, null); 59 | } else { 60 | Timber.e("Unknown status. Cannot set adapter view correctly"); 61 | statusLabel.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 62 | } 63 | 64 | return convertView; 65 | } 66 | 67 | public static float dipToPixels(Context context, float dipValue) { 68 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 69 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/fragment/MessagingFragment.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.fragment; 2 | 3 | 4 | import android.animation.ObjectAnimator; 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.util.Log; 11 | import android.view.KeyEvent; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.inputmethod.EditorInfo; 16 | import android.widget.EditText; 17 | import android.widget.TextView; 18 | 19 | import pro.dbro.ble.R; 20 | import pro.dbro.ble.data.DataStore; 21 | import pro.dbro.ble.ui.adapter.MessageAdapter; 22 | 23 | /** 24 | * A Fragment that currently allows chatting only in the public broadcast mode 25 | * ala Twitter. 26 | */ 27 | public class MessagingFragment extends Fragment implements MessageAdapter.MessageSelectedListener { 28 | public static final String TAG = "MessageListFragment"; 29 | 30 | public static interface ChatFragmentCallback { 31 | public void onMessageSendRequested(String message); 32 | public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId); 33 | } 34 | 35 | private ChatFragmentCallback mCallback; 36 | DataStore mDataStore; 37 | RecyclerView mRecyclerView; 38 | MessageAdapter mAdapter; 39 | EditText mMessageEntry; 40 | View mRoot; 41 | 42 | public MessagingFragment() { 43 | // Required empty public constructor 44 | } 45 | 46 | public void setDataStore(DataStore dataStore) { 47 | mDataStore = dataStore; 48 | } 49 | 50 | @Override 51 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 52 | Bundle savedInstanceState) { 53 | 54 | if (mDataStore == null) 55 | throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore"); 56 | 57 | // Inflate the layout for this fragment 58 | mRoot = inflater.inflate(R.layout.fragment_message, container, false); 59 | mMessageEntry = (EditText) mRoot.findViewById(R.id.messageEntry); 60 | mMessageEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() { 61 | @Override 62 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 63 | if (actionId == EditorInfo.IME_ACTION_SEND) { 64 | sendMessage(v.getText().toString()); 65 | v.setText(""); 66 | return true; 67 | } 68 | return false; 69 | } 70 | }); 71 | mRoot.findViewById(R.id.sendMessageButton).setOnClickListener(new View.OnClickListener() { 72 | @Override 73 | public void onClick(View v) { 74 | onSendMessageButtonClick(v); 75 | } 76 | }); 77 | mRecyclerView = (RecyclerView) mRoot.findViewById(R.id.recyclerView); 78 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 79 | mAdapter = new MessageAdapter(getActivity(), null, mDataStore, this, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER); 80 | mRecyclerView.setAdapter(mAdapter); 81 | return mRoot; 82 | } 83 | 84 | @Override 85 | public void onAttach(Activity activity) { 86 | super.onAttach(activity); 87 | try { 88 | mCallback = (ChatFragmentCallback) activity; 89 | } catch (ClassCastException e) { 90 | throw new ClassCastException(activity.toString() 91 | + " must implement ChatFragmentCallback"); 92 | } 93 | } 94 | 95 | public void onSendMessageButtonClick(View v) { 96 | sendMessage(mMessageEntry.getText().toString()); 97 | mMessageEntry.setText(""); 98 | } 99 | 100 | private void sendMessage(String message) { 101 | if (message.length() == 0) return; 102 | Log.i(TAG, "Sending message " + message); 103 | // For now treat all messsages as public broadcast 104 | mCallback.onMessageSendRequested(message); 105 | 106 | } 107 | 108 | @Override 109 | public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId) { 110 | mCallback.onMessageSelected(identiconView, usernameView, messageId, peerId); 111 | } 112 | 113 | public void animateIn() { 114 | mRoot.setAlpha(0); 115 | ObjectAnimator animator = ObjectAnimator.ofFloat(mRoot, "alpha", 0f, 1f) 116 | .setDuration(300); 117 | 118 | animator.setStartDelay(550); 119 | animator.start(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/fragment/ProfileFragment.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.fragment; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import pro.dbro.ble.R; 14 | import pro.dbro.ble.data.DataStore; 15 | import pro.dbro.ble.data.model.Peer; 16 | import pro.dbro.ble.ui.adapter.MessageAdapter; 17 | 18 | /** 19 | * A Fragment that displays all messages from a particular peer 20 | */ 21 | public class ProfileFragment extends Fragment { 22 | 23 | DataStore mDataStore; 24 | RecyclerView mRecyclerView; 25 | MessageAdapter mAdapter; 26 | Peer mFromPeer; 27 | 28 | // TextView mUsernameView; 29 | 30 | public static ProfileFragment createForPeer(@NonNull DataStore dataStore, 31 | @NonNull Peer peer) { 32 | 33 | ProfileFragment frag = new ProfileFragment(); 34 | frag.setFromPeer(peer); 35 | frag.setDataStore(dataStore); 36 | return frag; 37 | } 38 | 39 | public ProfileFragment() { 40 | // Required empty public constructor 41 | } 42 | 43 | public void setFromPeer(Peer fromPeer) { 44 | mFromPeer = fromPeer; 45 | } 46 | 47 | public void setDataStore(DataStore dataStore) { 48 | mDataStore = dataStore; 49 | } 50 | 51 | @Override 52 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 53 | Bundle savedInstanceState) { 54 | 55 | if (mDataStore == null) 56 | throw new IllegalStateException("MessageListFragment must be equipped with a DataStore. Did you call #setDataStore"); 57 | 58 | // Inflate the layout for this fragment 59 | final View root = inflater.inflate(R.layout.fragment_peer_profile, container, false); 60 | mRecyclerView = (RecyclerView) root.findViewById(R.id.recyclerView); 61 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 62 | mAdapter = new MessageAdapter(getActivity(), mFromPeer, mDataStore, null, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER); 63 | mRecyclerView.setAdapter(mAdapter); 64 | 65 | // SymmetricIdenticon identicon = (SymmetricIdenticon) root.findViewById(R.id.profile_identicon); 66 | // ((SymmetricIdenticon) root.findViewById(R.id.profile_identicon)).show(new String(mFromPeer.getPublicKey())); 67 | // mUsernameView = ((TextView) root.findViewById(R.id.profile_name)); 68 | // mUsernameView.setText(mFromPeer.getAlias()); 69 | 70 | return root; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/pro/dbro/ble/ui/fragment/WelcomeFragment.java: -------------------------------------------------------------------------------- 1 | package pro.dbro.ble.ui.fragment; 2 | 3 | 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.support.v4.app.Fragment; 7 | import android.view.KeyEvent; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.EditText; 12 | import android.widget.TextView; 13 | 14 | import pro.dbro.ble.R; 15 | 16 | public class WelcomeFragment extends Fragment { 17 | 18 | public interface WelcomeFragmentCallback { 19 | public void onNameChosen(String name); 20 | } 21 | 22 | private WelcomeFragmentCallback mCallback; 23 | 24 | public WelcomeFragment() { 25 | // Required empty public constructor 26 | } 27 | 28 | @Override 29 | public void onAttach(Activity activity) { 30 | super.onAttach(activity); 31 | try { 32 | mCallback = (WelcomeFragmentCallback) activity; 33 | } catch (ClassCastException e) { 34 | throw new ClassCastException(activity.toString() + " must implement WelcomeFragmentCallback"); 35 | } 36 | } 37 | 38 | @Override 39 | public void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | } 42 | 43 | @Override 44 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 45 | Bundle savedInstanceState) { 46 | View root = inflater.inflate(R.layout.fragment_welcome, container, false); 47 | ((EditText) root.findViewById(R.id.aliasEntry)).setOnEditorActionListener(new TextView.OnEditorActionListener() { 48 | @Override 49 | public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { 50 | mCallback.onNameChosen(textView.getText().toString()); 51 | return false; 52 | } 53 | }); 54 | return root; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnlyInAmerica/BLEMeshChat/d6dbf0c3c6ed0ebc9c9567c64468e3e1597e1414/app/src/main/res/drawable-xxhdpi/ic_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnlyInAmerica/BLEMeshChat/d6dbf0c3c6ed0ebc9c9567c64468e3e1597e1414/app/src/main/res/drawable-xxhdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_user_white_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnlyInAmerica/BLEMeshChat/d6dbf0c3c6ed0ebc9c9567c64468e3e1597e1414/app/src/main/res/drawable-xxhdpi/ic_user_white_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/sneakernet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnlyInAmerica/BLEMeshChat/d6dbf0c3c6ed0ebc9c9567c64468e3e1597e1414/app/src/main/res/drawable/sneakernet.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/status_always_online.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/status_offline.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/status_online_in_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/transparent_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 40 | 47 | 48 | 49 | 50 | 53 | 54 | 67 | 68 | 74 | 75 | 86 | 87 | 95 | 96 | 102 | 103 | 110 | 111 | 116 | 117 | 123 | 124 | 125 | 126 | 136 | 137 | 142 | 143 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 165 | 166 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_welcome.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_message.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 |