├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ └── output.json └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── left │ │ └── meshim │ │ └── database │ │ └── MigrationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── io │ │ └── left │ │ └── meshim │ │ ├── activities │ │ └── IActivity.aidl │ │ ├── models │ │ ├── ConversationSummary.aidl │ │ ├── Message.aidl │ │ └── User.aidl │ │ └── services │ │ └── IMeshIMService.aidl │ ├── assets │ └── in_range_loading_animation.json │ ├── java │ └── io │ │ └── left │ │ └── meshim │ │ ├── activities │ │ ├── ChatActivity.java │ │ ├── ChooseAvatarActivity.java │ │ ├── LaunchActivity.java │ │ ├── MainActivity.java │ │ ├── OnboardingUsernameActivity.java │ │ └── ServiceConnectedActivity.java │ │ ├── adapters │ │ ├── ConversationListAdapter.java │ │ ├── MessageListAdapter.java │ │ └── OnlineUserListAdapter.java │ │ ├── controllers │ │ └── RightMeshController.java │ │ ├── database │ │ ├── Converters.java │ │ ├── MeshIMDao.java │ │ ├── MeshIMDatabase.java │ │ └── Migrations.java │ │ ├── models │ │ ├── ConversationSummary.java │ │ ├── MeshIdTuple.java │ │ ├── Message.java │ │ ├── Settings.java │ │ └── User.java │ │ └── services │ │ └── MeshIMService.java │ ├── proto │ └── MeshIMMessages.proto │ ├── res │ ├── color │ │ └── tabcolor.xml │ ├── drawable │ │ ├── avatar_select.xml │ │ ├── chat_bubble_rec.xml │ │ ├── chat_bubble_send.xml │ │ ├── ic_account_active.xml │ │ ├── ic_done_all_black_24dp.xml │ │ ├── ic_done_black_24dp.xml │ │ ├── ic_in_range_active.xml │ │ ├── ic_keyboard_arrow_right_black_24dp.xml │ │ ├── ic_message_active.xml │ │ ├── round_corner.xml │ │ ├── scrollbar.xml │ │ ├── splash_background.xml │ │ ├── tab_background.xml │ │ └── tab_indicator.xml │ ├── layout │ │ ├── activity_chat.xml │ │ ├── activity_choose_avatar.xml │ │ ├── activity_main.xml │ │ ├── activity_onboarding_username.xml │ │ ├── dialog_change_username.xml │ │ ├── list_item_conversation.xml │ │ ├── list_item_message_received.xml │ │ ├── list_item_message_sent.xml │ │ ├── list_item_online_user.xml │ │ ├── setting_page.xml │ │ ├── tab_layout.xml │ │ └── table_row_choose_avatar.xml │ ├── menu │ │ └── menu_main_screen.xml │ ├── mipmap-hdpi │ │ ├── account_default.png │ │ ├── available_icon.png │ │ ├── avatar1.png │ │ ├── avatar10.png │ │ ├── avatar11.png │ │ ├── avatar12.png │ │ ├── avatar13.png │ │ ├── avatar14.png │ │ ├── avatar15.png │ │ ├── avatar16.png │ │ ├── avatar17.png │ │ ├── avatar18.png │ │ ├── avatar19.png │ │ ├── avatar2.png │ │ ├── avatar20.png │ │ ├── avatar21.png │ │ ├── avatar22.png │ │ ├── avatar23.png │ │ ├── avatar24.png │ │ ├── avatar25.png │ │ ├── avatar26.png │ │ ├── avatar27.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ ├── avatar5.png │ │ ├── avatar6.png │ │ ├── avatar7.png │ │ ├── avatar8.png │ │ ├── avatar9.png │ │ ├── edit.png │ │ ├── ic_arrow_back_white_24dp.png │ │ ├── ic_launcher.png │ │ ├── im_header.png │ │ ├── in_range_default.png │ │ ├── invite.png │ │ ├── meshim_wordmark.png │ │ ├── messages_default.png │ │ ├── no_messages.png │ │ ├── rightmesh_power.png │ │ ├── rightmesh_small.png │ │ ├── rm_launcher.png │ │ ├── send_button.png │ │ └── splash_logo_main.png │ ├── mipmap-mdpi │ │ ├── account_default.png │ │ ├── available_icon.png │ │ ├── avatar1.png │ │ ├── avatar10.png │ │ ├── avatar11.png │ │ ├── avatar12.png │ │ ├── avatar13.png │ │ ├── avatar14.png │ │ ├── avatar15.png │ │ ├── avatar16.png │ │ ├── avatar17.png │ │ ├── avatar18.png │ │ ├── avatar19.png │ │ ├── avatar2.png │ │ ├── avatar20.png │ │ ├── avatar21.png │ │ ├── avatar22.png │ │ ├── avatar23.png │ │ ├── avatar24.png │ │ ├── avatar25.png │ │ ├── avatar26.png │ │ ├── avatar27.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ ├── avatar5.png │ │ ├── avatar6.png │ │ ├── avatar7.png │ │ ├── avatar8.png │ │ ├── avatar9.png │ │ ├── edit.png │ │ ├── ic_arrow_back_white_24dp.png │ │ ├── ic_launcher.png │ │ ├── im_header.png │ │ ├── in_range_default.png │ │ ├── invite.png │ │ ├── meshim_wordmark.png │ │ ├── messages_default.png │ │ ├── no_messages.png │ │ ├── rightmesh_power.png │ │ ├── rightmesh_small.png │ │ ├── rm_launcher.png │ │ ├── send_button.png │ │ └── splash_logo_main.png │ ├── mipmap-xhdpi │ │ ├── account_default.png │ │ ├── available_icon.png │ │ ├── avatar1.png │ │ ├── avatar10.png │ │ ├── avatar11.png │ │ ├── avatar12.png │ │ ├── avatar13.png │ │ ├── avatar14.png │ │ ├── avatar15.png │ │ ├── avatar16.png │ │ ├── avatar17.png │ │ ├── avatar18.png │ │ ├── avatar19.png │ │ ├── avatar2.png │ │ ├── avatar20.png │ │ ├── avatar21.png │ │ ├── avatar22.png │ │ ├── avatar23.png │ │ ├── avatar24.png │ │ ├── avatar25.png │ │ ├── avatar26.png │ │ ├── avatar27.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ ├── avatar5.png │ │ ├── avatar6.png │ │ ├── avatar7.png │ │ ├── avatar8.png │ │ ├── avatar9.png │ │ ├── edit.png │ │ ├── ic_arrow_back_white_24dp.png │ │ ├── ic_launcher.png │ │ ├── im_header.png │ │ ├── in_range_default.png │ │ ├── invite.png │ │ ├── meshim_wordmark.png │ │ ├── messages_default.png │ │ ├── no_messages.png │ │ ├── rightmesh_power.png │ │ ├── rightmesh_small.png │ │ ├── rm_launcher.png │ │ ├── send_button.png │ │ └── splash_logo_main.png │ ├── mipmap-xxhdpi │ │ ├── account_default.png │ │ ├── available_icon.png │ │ ├── avatar1.png │ │ ├── avatar10.png │ │ ├── avatar11.png │ │ ├── avatar12.png │ │ ├── avatar13.png │ │ ├── avatar14.png │ │ ├── avatar15.png │ │ ├── avatar16.png │ │ ├── avatar17.png │ │ ├── avatar18.png │ │ ├── avatar19.png │ │ ├── avatar2.png │ │ ├── avatar20.png │ │ ├── avatar21.png │ │ ├── avatar22.png │ │ ├── avatar23.png │ │ ├── avatar24.png │ │ ├── avatar25.png │ │ ├── avatar26.png │ │ ├── avatar27.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ ├── avatar5.png │ │ ├── avatar6.png │ │ ├── avatar7.png │ │ ├── avatar8.png │ │ ├── avatar9.png │ │ ├── edit.png │ │ ├── ic_arrow_back_white_24dp.png │ │ ├── ic_launcher.png │ │ ├── im_header.png │ │ ├── in_range_default.png │ │ ├── invite.png │ │ ├── meshim_wordmark.png │ │ ├── messages_default.png │ │ ├── no_messages.png │ │ ├── rightmesh_power.png │ │ ├── rightmesh_small.png │ │ ├── rm_launcher.png │ │ ├── send_button.png │ │ └── splash_logo_main.png │ ├── mipmap-xxxhdpi │ │ ├── account_default.png │ │ ├── available_icon.png │ │ ├── avatar1.png │ │ ├── avatar10.png │ │ ├── avatar11.png │ │ ├── avatar12.png │ │ ├── avatar13.png │ │ ├── avatar14.png │ │ ├── avatar15.png │ │ ├── avatar16.png │ │ ├── avatar17.png │ │ ├── avatar18.png │ │ ├── avatar19.png │ │ ├── avatar2.png │ │ ├── avatar20.png │ │ ├── avatar21.png │ │ ├── avatar22.png │ │ ├── avatar23.png │ │ ├── avatar24.png │ │ ├── avatar25.png │ │ ├── avatar26.png │ │ ├── avatar27.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ ├── avatar5.png │ │ ├── avatar6.png │ │ ├── avatar7.png │ │ ├── avatar8.png │ │ ├── avatar9.png │ │ ├── edit.png │ │ ├── ic_arrow_back_white_24dp.png │ │ ├── ic_launcher.png │ │ ├── im_header.png │ │ ├── in_range_default.png │ │ ├── invite.png │ │ ├── meshim_wordmark.png │ │ ├── messages_default.png │ │ ├── no_messages.png │ │ ├── rightmesh_power.png │ │ ├── rightmesh_small.png │ │ ├── rm_launcher.png │ │ ├── send_button.png │ │ └── splash_logo_main.png │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── schemas │ └── io.left.meshim.database.MeshIMDatabase │ ├── 1.json │ ├── 2.json │ ├── 3.json │ ├── 4.json │ ├── 5.json │ └── 6.json ├── build.gradle ├── config └── checkstyle │ └── suppressions.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't version any IDE files. 2 | .idea/ 3 | 4 | # Externally managed Checkstyle config file 5 | config/checkstyle/checkstyle.xml 6 | 7 | # The remainder of this file is generated by https://gitignore.io, using their settings for Android, 8 | # Git, Gradle, Intellij, Linux, macOS, and Windows, with the following edits: 9 | # - The "Intellij Patch" section is uncommented. 10 | # - The workaround in the Gradle section is deleted. 11 | # If you add a new framework to the project (or if one we use is missing from this list)be sure to 12 | # check https://gitignore.io for a configuration. 13 | 14 | ### Android ### 15 | # Built application files 16 | *.apk 17 | *.ap_ 18 | 19 | # Files for the ART/Dalvik VM 20 | *.dex 21 | 22 | # Java class files 23 | *.class 24 | 25 | # Generated files 26 | bin/ 27 | gen/ 28 | out/ 29 | 30 | # Gradle files 31 | .gradle/ 32 | build/ 33 | 34 | # Local configuration file (sdk path, etc) 35 | local.properties 36 | 37 | # Proguard folder generated by Eclipse 38 | proguard/ 39 | 40 | # Log Files 41 | *.log 42 | 43 | # Android Studio Navigation editor temp files 44 | .navigation/ 45 | 46 | # Android Studio captures folder 47 | captures/ 48 | 49 | # Intellij 50 | *.iml 51 | .idea/workspace.xml 52 | .idea/tasks.xml 53 | .idea/gradle.xml 54 | .idea/libraries 55 | 56 | # Keystore files 57 | *.jks 58 | 59 | # External native build folder generated in Android Studio 2.2 and later 60 | .externalNativeBuild 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | ### Android Patch ### 71 | gen-external-apklibs 72 | 73 | ### Git ### 74 | *.orig 75 | 76 | ### Intellij ### 77 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 78 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 79 | 80 | # User-specific stuff: 81 | .idea/**/workspace.xml 82 | .idea/**/tasks.xml 83 | 84 | # Sensitive or high-churn files: 85 | .idea/**/dataSources/ 86 | .idea/**/dataSources.ids 87 | .idea/**/dataSources.xml 88 | .idea/**/dataSources.local.xml 89 | .idea/**/sqlDataSources.xml 90 | .idea/**/dynamic.xml 91 | .idea/**/uiDesigner.xml 92 | 93 | # Gradle: 94 | .idea/**/gradle.xml 95 | .idea/**/libraries 96 | 97 | # Mongo Explorer plugin: 98 | .idea/**/mongoSettings.xml 99 | 100 | ## File-based project format: 101 | *.iws 102 | 103 | ## Plugin-specific files: 104 | 105 | # IntelliJ 106 | /out/ 107 | 108 | # mpeltonen/sbt-idea plugin 109 | .idea_modules/ 110 | 111 | # JIRA plugin 112 | atlassian-ide-plugin.xml 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | ### Intellij Patch ### 121 | *.iml 122 | modules.xml 123 | .idea/misc.xml 124 | *.ipr 125 | 126 | ### Linux ### 127 | *~ 128 | 129 | # temporary files which can be created if a process still has a handle open of a deleted file 130 | .fuse_hidden* 131 | 132 | # KDE directory preferences 133 | .directory 134 | 135 | # Linux trash folder which might appear on any partition or disk 136 | .Trash-* 137 | 138 | # .nfs files are created when an open file is removed but is still being accessed 139 | .nfs* 140 | 141 | ### macOS ### 142 | *.DS_Store 143 | .AppleDouble 144 | .LSOverride 145 | 146 | # Icon must end with two \r 147 | Icon 148 | 149 | 150 | # Thumbnails 151 | ._* 152 | 153 | # Files that might appear in the root of a volume 154 | .DocumentRevisions-V100 155 | .fseventsd 156 | .Spotlight-V100 157 | .TemporaryItems 158 | .Trashes 159 | .VolumeIcon.icns 160 | .com.apple.timemachine.donotpresent 161 | 162 | # Directories potentially created on remote AFP share 163 | .AppleDB 164 | .AppleDesktop 165 | Network Trash Folder 166 | Temporary Items 167 | .apdisk 168 | 169 | ### Windows ### 170 | # Windows thumbnail cache files 171 | Thumbs.db 172 | ehthumbs.db 173 | ehthumbs_vista.db 174 | 175 | # Folder config file 176 | Desktop.ini 177 | 178 | # Recycle Bin used on file shares 179 | $RECYCLE.BIN/ 180 | 181 | # Windows Installer files 182 | *.cab 183 | *.msi 184 | *.msm 185 | *.msp 186 | 187 | # Windows shortcuts 188 | *.lnk 189 | 190 | ### Gradle ### 191 | .gradle 192 | /build/ 193 | 194 | # Ignore Gradle GUI config 195 | gradle-app.setting 196 | 197 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 198 | !gradle-wrapper.jar 199 | 200 | # Cache of project 201 | .gradletasknamecache 202 | 203 | # license 204 | app/src/main/assets/rightmesh.json 205 | 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meshIM 2 | 3 | meshIM is an on- or off-line Android messaging application built on [the RightMesh library](https://rightmesh.io). 4 | 5 | You can send messages to other meshIM users over the internet, or form automatically form a local network to send messages to devices nearby! 6 | 7 | ## RightMesh 8 | 9 | RightMesh is a software-based, mobile mesh networking platform and protocol using blockchain technology and tokens to enhance global connectivity. Find out more at [RightMesh.io](https://rightmesh.io) 10 | 11 | ## Building meshIM 12 | 13 | You can clone this repository and build meshIM in Android Studio. You will need your own RightMesh developer credentials, license key, and mesh port to build and run the application. Check out the [RightMesh DeveloperPortal](https://developer.rightmesh.io) to sign up and find instructions. 14 | 15 | The official meshIM key is as follows 16 | >> rightmesh_meshim_key="0x4282c537813c21fcfb59860a89c94546564635f5" 17 | 18 | This key makes use of the official meshIM Mesh Ports and can only be used by approved developers. You may swap the key out for your own to build meshIM, however you will not be able to communicate with users running an official meshIM build. 19 | 20 | ## Dependencies 21 | 22 | Aside from RightMesh, meshIM uses [Gson](https://github.com/google/gson) for JSON serialization and Room for database abstraction, as well as some other Google support libraries for backwards compatibility. 23 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | wave.json -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'io.left.rightmesh.rightmesh-plugin' version '1.8.1' 4 | id 'com.google.protobuf' 5 | } 6 | 7 | rightmesh { 8 | appKey = rightmesh_meshim_key 9 | username = rightmesh_build_username 10 | password = rightmesh_build_password 11 | } 12 | 13 | preBuild.dependsOn("rightmesh") 14 | repositories { 15 | mavenCentral() 16 | jcenter() 17 | google() 18 | maven { 19 | url "https://artifactory.rightmesh.io/artifactory/maven" 20 | credentials { 21 | username rightmesh_build_username 22 | password rightmesh_build_password 23 | } 24 | } 25 | maven{ 26 | url "https://dl.bintray.com/ethereum/maven" 27 | } 28 | } 29 | 30 | android { 31 | compileSdkVersion 28 32 | buildToolsVersion '28.0.3' 33 | defaultConfig { 34 | applicationId 'io.left.meshim' 35 | minSdkVersion 14 36 | targetSdkVersion 28 37 | versionCode 3 38 | versionName "0.2.1" 39 | multiDexEnabled true 40 | javaCompileOptions { 41 | annotationProcessorOptions { 42 | arguments = ["room.schemaLocation": "$projectDir/src/main/schemas".toString()] 43 | } 44 | } 45 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 46 | } 47 | buildTypes { 48 | release { 49 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 50 | } 51 | } 52 | compileOptions { 53 | targetCompatibility JavaVersion.VERSION_1_8 54 | sourceCompatibility JavaVersion.VERSION_1_8 55 | } 56 | sourceSets { 57 | androidTest.assets.srcDirs += files("$projectDir/src/main/schemas".toString()) 58 | main { assets.srcDirs = ['src/main/assets', 'src/main/assets/2'] } 59 | } 60 | productFlavors { 61 | } 62 | } 63 | 64 | protobuf { 65 | protoc { 66 | artifact = 'com.google.protobuf:protoc:3.0.0' 67 | } 68 | plugins { 69 | javalite { 70 | artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' 71 | } 72 | } 73 | generateProtoTasks { 74 | all().each { task -> 75 | task.builtins { 76 | remove java 77 | } 78 | task.plugins { 79 | javalite {} 80 | } 81 | } 82 | } 83 | } 84 | 85 | dependencies { 86 | implementation 'android.arch.persistence.room:runtime:1.1.1' 87 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 88 | implementation 'com.android.support:design:28.0.0' 89 | implementation 'com.android.support:support-annotations:28.0.0' 90 | implementation 'com.android.support:support-v13:28.0.0' 91 | 92 | annotationProcessor "android.arch.persistence.room:compiler:1.1.1" 93 | 94 | implementation 'com.android.support:multidex:1.0.3' 95 | implementation 'com.android.support:appcompat-v7:28.0.0' 96 | implementation 'com.android.support:recyclerview-v7:28.0.0' 97 | implementation 'com.google.code.gson:gson:2.8.4' 98 | implementation 'io.left.rightmesh:lib-rightmesh-android:0.10.0' 99 | implementation fileTree(include: ['*.jar'], dir: 'libs') 100 | 101 | androidTestImplementation 'com.android.support:support-annotations:28.0.0' 102 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 103 | androidTestImplementation 'android.arch.persistence.room:testing:1.1.1' 104 | implementation 'com.airbnb.android:lottie:2.5.0-rc1' 105 | } 106 | -------------------------------------------------------------------------------- /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 /home/jason/Android/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/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":3},"path":"app-release.apk","properties":{"packageId":"io.left.meshim","split":"","minSdkVersion":"14"}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/io/left/meshim/database/MigrationTest.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.database; 2 | 3 | import static io.left.meshim.database.Migrations.MIGRATION_1_2; 4 | import static io.left.meshim.database.Migrations.MIGRATION_2_3; 5 | import static io.left.meshim.database.Migrations.MIGRATION_3_4; 6 | import static io.left.meshim.database.Migrations.MIGRATION_4_5; 7 | import static io.left.meshim.database.Migrations.MIGRATION_5_6; 8 | 9 | import android.arch.persistence.db.SupportSQLiteDatabase; 10 | import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; 11 | import android.arch.persistence.room.testing.MigrationTestHelper; 12 | import android.database.Cursor; 13 | import android.support.test.InstrumentationRegistry; 14 | import android.support.test.runner.AndroidJUnit4; 15 | 16 | import io.left.rightmesh.id.MeshId; 17 | 18 | import java.io.IOException; 19 | 20 | import org.junit.Assert; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | 25 | /** 26 | * Verifies that Room schema migrations work. 27 | */ 28 | @RunWith(AndroidJUnit4.class) 29 | public class MigrationTest { 30 | private static final String TEST_DB = "migration-test"; 31 | 32 | @Rule 33 | public MigrationTestHelper helper; 34 | 35 | /** 36 | * Initializes {@link MigrationTest#helper}. 37 | */ 38 | public MigrationTest() { 39 | helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), 40 | MeshIMDatabase.class.getCanonicalName(), 41 | new FrameworkSQLiteOpenHelperFactory()); 42 | } 43 | 44 | /** 45 | * Renamed several columns in Users table. Verifies that values survive the migration. 46 | * 47 | * @throws IOException if the database can't open 48 | */ 49 | @Test 50 | public void migrate1To2() throws IOException { 51 | // Dummy values. 52 | int id = 42; 53 | MeshId meshId = new MeshId(); 54 | String meshIdUuid = meshId.toString().substring(2); // String representation for insertion. 55 | String userName = "John"; 56 | int avatar = 2; 57 | 58 | // Insert dummy values into database manually. 59 | SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); 60 | db.execSQL("INSERT INTO Users (UserID, UserMeshID, UserName, UserAvatar)" 61 | + " VALUES ( " + id + ", X'" + meshIdUuid + "', \"" + userName + "\", " 62 | + avatar + ")"); 63 | db.close(); 64 | 65 | // Update and validate database. 66 | db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); 67 | Cursor cursor 68 | = db.query("SELECT UserID, MeshID, Username, Avatar FROM Users WHERE UserID = 42"); 69 | 70 | // Assert the row was found. 71 | Assert.assertEquals("Row is found", cursor.getCount(), 1); 72 | cursor.moveToNext(); 73 | 74 | // Check values were preserved. 75 | Assert.assertArrayEquals("MeshID is preserved", cursor.getBlob(1), meshId.getRawMeshId()); 76 | Assert.assertEquals("Username is preserved", cursor.getString(2), userName); 77 | Assert.assertEquals("Avatar is preserved", cursor.getInt(3), avatar); 78 | } 79 | 80 | /** 81 | * Addition of Messages table - as long as schema verifies it should be fine. 82 | * 83 | * @throws IOException if the database can't open 84 | */ 85 | @Test 86 | public void migrate2To3() throws IOException { 87 | SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2); 88 | db = helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3); 89 | } 90 | 91 | /** 92 | * Addition of IsRead column in the Messages table. sets the default values for IsRead to true 93 | * for messages before the migration. 94 | * @throws IOException if the database cant open. 95 | */ 96 | @Test 97 | public void migrate3To4() throws IOException { 98 | SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 3); 99 | db = helper.runMigrationsAndValidate(TEST_DB, 4, true, MIGRATION_3_4); 100 | } 101 | 102 | /** 103 | * Addition of IsDelivered column in the Messages table. sets the default values for IsDelivered 104 | * to true for messages before the migration. 105 | * @throws IOException if the database cant open. 106 | */ 107 | @Test 108 | public void migrate4To5() throws IOException { 109 | SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4); 110 | db = helper.runMigrationsAndValidate(TEST_DB, 5, true, MIGRATION_4_5); 111 | } 112 | 113 | /** 114 | * @throws IOException if the database cant open. 115 | */ 116 | @Test 117 | public void migrate5to6() throws IOException { 118 | 119 | // Dummy values. 120 | int id = 42; 121 | MeshId meshId = new MeshId(); 122 | String meshIdUuid = meshId.toString().substring(2); // String representation for insertion. 123 | String userName = "John"; 124 | int avatar = 2; 125 | int senderId = 10; 126 | int recipientId = 11; 127 | 128 | // Insert dummy values into database manually. 129 | SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 5); 130 | db.execSQL("INSERT INTO Users (UserID, MeshID, Username, Avatar)" 131 | + " VALUES ( " + id + ", X'" + meshIdUuid + "', \"" + userName + "\", " 132 | + avatar + ")"); 133 | db.execSQL("INSERT INTO Messages (MessageId, Contents, Timestamp, SenderId, RecipientId," + 134 | "SentFromDevice, IsRead, IsDelivered)" + 135 | "VALUES ( " + id + ", \"Hello World\", " + 1 + ", " + senderId + ", " + recipientId 136 | + ", " + 1 + ", " + 1 + ", " + 1 + ")"); 137 | 138 | db.close(); 139 | db = helper.runMigrationsAndValidate(TEST_DB, 6, true, MIGRATION_5_6); 140 | 141 | // Check User values 142 | Cursor cursor 143 | = db.query("SELECT UserId, MeshId, Username, Avatar FROM Users WHERE UserId = 42"); 144 | Assert.assertEquals("Row is found", cursor.getCount(), 1); 145 | cursor.moveToNext(); 146 | Assert.assertArrayEquals("MeshId is preserved", cursor.getBlob(1), meshId.getRawMeshId()); 147 | 148 | // Check Message values. 149 | cursor = db.query("SELECT MessageId, SenderId, RecipientId FROM Messages " 150 | + "WHERE MessageId = 42"); 151 | Assert.assertEquals("Row is found", cursor.getCount(), 1); 152 | cursor.moveToNext(); 153 | Assert.assertEquals("MessageId is preserved", id, cursor.getInt(0)); 154 | Assert.assertEquals("SenderId is preserved", senderId, cursor.getInt(1)); 155 | Assert.assertEquals("RecipientId is preserved", recipientId, cursor.getInt(2)); 156 | } 157 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 29 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/aidl/io/left/meshim/activities/IActivity.aidl: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | interface IActivity { 4 | void updateInterface(); 5 | } -------------------------------------------------------------------------------- /app/src/main/aidl/io/left/meshim/models/ConversationSummary.aidl: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | parcelable ConversationSummary; -------------------------------------------------------------------------------- /app/src/main/aidl/io/left/meshim/models/Message.aidl: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | parcelable Message; -------------------------------------------------------------------------------- /app/src/main/aidl/io/left/meshim/models/User.aidl: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | parcelable User; -------------------------------------------------------------------------------- /app/src/main/aidl/io/left/meshim/services/IMeshIMService.aidl: -------------------------------------------------------------------------------- 1 | package io.left.meshim.services; 2 | 3 | import io.left.meshim.activities.IActivity; 4 | import io.left.meshim.models.ConversationSummary; 5 | import io.left.meshim.models.Message; 6 | import io.left.meshim.models.User; 7 | 8 | interface IMeshIMService { 9 | // Service funcionality. 10 | 11 | void setForeground(in boolean value); 12 | 13 | 14 | // RightMesh funcionality. 15 | 16 | void broadcastUpdatedProfile(); 17 | 18 | List getOnlineUsers(); 19 | 20 | void registerActivityCallback(in IActivity callback); 21 | 22 | void sendTextMessage(in User recipient, in String message); 23 | 24 | void showRightMeshSettings(); 25 | 26 | 27 | // Database queries. 28 | 29 | List fetchConversationSummaries(); 30 | 31 | List fetchMessagesForUser(in User user); 32 | 33 | User fetchUserById(in int id); 34 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/activities/ChatActivity.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.os.DeadObjectException; 6 | import android.os.RemoteException; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.text.Spannable; 10 | import android.text.SpannableString; 11 | import android.text.style.ForegroundColorSpan; 12 | import android.view.MenuItem; 13 | import android.widget.EditText; 14 | import android.widget.ImageButton; 15 | 16 | import io.left.meshim.R; 17 | import io.left.meshim.adapters.MessageListAdapter; 18 | import io.left.meshim.models.User; 19 | 20 | /** 21 | * An activity that displays a conversation between two users, and enables sending messages. 22 | */ 23 | public class ChatActivity extends ServiceConnectedActivity { 24 | private RecyclerView mMessageListView; 25 | private MessageListAdapter mMessageListAdapter; 26 | User mRecipient; 27 | 28 | /** 29 | * {@inheritDoc}. 30 | */ 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_chat); 35 | 36 | // Null-check recipient. 37 | mRecipient = getIntent().getParcelableExtra("recipient"); 38 | if (mRecipient == null) { 39 | finish(); 40 | } 41 | 42 | // Set up the message adapter. 43 | mMessageListAdapter = new MessageListAdapter(mRecipient); 44 | try { 45 | mMessageListAdapter.updateList(this.mService); 46 | } catch (DeadObjectException e) { 47 | reconnectToService(); 48 | } 49 | 50 | // Initialize the list view for the messages. 51 | mMessageListView = findViewById(R.id.reyclerview_message_list); 52 | mMessageListView.setNestedScrollingEnabled(false); 53 | mMessageListView.setLayoutManager(new LinearLayoutManager(this)); 54 | mMessageListView.setAdapter(mMessageListAdapter); 55 | 56 | // Connect the send button to the service. 57 | ImageButton sendButton = findViewById(R.id.sendButton); 58 | EditText messageText = findViewById(R.id.myMessageEditText); 59 | sendButton.setOnClickListener(view -> { 60 | if (mService != null) { 61 | try { 62 | String message = messageText.getText().toString(); 63 | if (!message.equals("")) { 64 | mService.sendTextMessage(mRecipient, message); 65 | messageText.setText(""); 66 | } 67 | } catch (RemoteException re) { 68 | if (re instanceof DeadObjectException) { 69 | reconnectToService(); 70 | } 71 | // Otherwise ignore - we don't clear the message out, so the user can attempt 72 | // to re-send it if they would like. 73 | } 74 | } 75 | }); 76 | setupActionBar(); 77 | 78 | } 79 | 80 | /** 81 | * Sets up the action bar to display user name and back button. 82 | */ 83 | private void setupActionBar() { 84 | if (mRecipient != null && getSupportActionBar() != null) { 85 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 86 | getSupportActionBar().setDisplayShowHomeEnabled(true); 87 | getSupportActionBar().setHomeAsUpIndicator(R.mipmap.ic_arrow_back_white_24dp); 88 | String title = mRecipient.getUsername(); 89 | SpannableString s = new SpannableString(title); 90 | s.setSpan(new ForegroundColorSpan(Color.WHITE), 0, title.length(), 91 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 92 | getSupportActionBar().setTitle(s); 93 | } 94 | } 95 | 96 | /** 97 | * {@inheritDoc}. 98 | */ 99 | @Override 100 | void updateInterface() { 101 | runOnUiThread(() -> { 102 | try { 103 | mMessageListAdapter.updateList(mService); 104 | mMessageListAdapter.notifyDataSetChanged(); 105 | int index = mMessageListAdapter.getItemCount() - 1; 106 | if (index > -1) { 107 | mMessageListView.smoothScrollToPosition(index); 108 | } 109 | } catch (DeadObjectException e) { 110 | reconnectToService(); 111 | } 112 | }); 113 | } 114 | 115 | @Override 116 | public boolean onOptionsItemSelected(MenuItem item) { 117 | int i = item.getItemId(); 118 | if (i == android.R.id.home) { 119 | this.finish(); 120 | } 121 | return super.onOptionsItemSelected(item); 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/activities/ChooseAvatarActivity.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.ImageButton; 9 | import android.widget.ScrollView; 10 | import android.widget.TableLayout; 11 | import android.widget.TableRow; 12 | 13 | import io.left.meshim.R; 14 | import io.left.meshim.models.User; 15 | 16 | /** 17 | * User avatar selection interface. 18 | */ 19 | public class ChooseAvatarActivity extends AppCompatActivity { 20 | public static final String ONBOARDING_ACTION = "from onboarding"; 21 | 22 | //used for the table layout 23 | private static final int ROWS = 9; 24 | private static final int COLUMNS = 3; 25 | 26 | private Button mSaveButton; 27 | private ImageButton mSelectedAvatar; 28 | private int mUserAvatarId = R.mipmap.account_default; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_choose_avatar); 34 | 35 | // Populate list of avatars. 36 | setupAvatars(); 37 | 38 | // Save reference to buttons for use when avatars are selected 39 | mSaveButton = findViewById(R.id.saveUserAvatarButton); 40 | mSelectedAvatar = findViewById(R.id.choose_avatar_selected_avatar); 41 | } 42 | 43 | /** 44 | * Update user and launch next activity when save button is tapped. 45 | * @param view save button 46 | */ 47 | public void saveAvatar(View view) { 48 | User user = User.fromDisk(this); 49 | if (user == null) { 50 | // Initialize if not found, so we can still save the user's selection. 51 | user = new User(); 52 | } 53 | user.setAvatar(mUserAvatarId); 54 | user.save(this); 55 | 56 | // Launch app if called from onboarding activity. 57 | String action = getIntent().getAction(); 58 | if (action != null && action.equals(ONBOARDING_ACTION)) { 59 | Intent intent = new Intent(ChooseAvatarActivity.this, MainActivity.class); 60 | startActivity(intent); 61 | } 62 | 63 | finish(); 64 | } 65 | 66 | /** 67 | * The function finds the scrollview in the layout and creates a dynamic 68 | * table layout of avatars user can choose from. 69 | */ 70 | private void setupAvatars() { 71 | //keep track of the avatars 72 | int avatarNum = 1; 73 | 74 | ScrollView scrollView = findViewById(R.id.avatarScrollView); 75 | TableLayout tableLayout = new TableLayout(this); 76 | 77 | // Populate table. 78 | for (int r = 0; r < ROWS; r++) { 79 | // Define a new row. 80 | TableRow tableRow = (TableRow) getLayoutInflater() 81 | .inflate(R.layout.table_row_choose_avatar, tableLayout, false); 82 | 83 | // Populate row with images. 84 | for (int c = 0; c < COLUMNS; c++) { 85 | final ImageButton imageButton = (ImageButton) tableRow.getChildAt(c); 86 | final int id = getResources().getIdentifier("avatar" + avatarNum, "mipmap", getPackageName()); 87 | imageButton.setImageResource(id); 88 | 89 | imageButton.setOnClickListener(v -> { 90 | mUserAvatarId = id; 91 | mSelectedAvatar.setImageResource(mUserAvatarId); 92 | mSaveButton.setClickable(true); 93 | }); 94 | avatarNum++; 95 | } 96 | tableLayout.addView(tableRow); 97 | } 98 | scrollView.addView(tableLayout); 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/activities/LaunchActivity.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import io.left.meshim.models.Settings; 8 | import io.left.meshim.models.User; 9 | 10 | /** 11 | * Class that launches the application. Checks whether the user needs to be onboarded or not, 12 | * and launches the next appropriate Activity. 13 | */ 14 | public class LaunchActivity extends AppCompatActivity { 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | // Initialize settings if not found. 20 | Settings settings = Settings.fromDisk(this); 21 | if (settings == null) { 22 | settings = new Settings(); 23 | settings.save(this); 24 | } 25 | 26 | Intent intent; 27 | //checking if we already have a user profile 28 | if (User.fromDisk(this) != null) { 29 | intent = new Intent(LaunchActivity.this, MainActivity.class); 30 | } else { 31 | // Launch first time activity to create a new profile 32 | intent = new Intent(LaunchActivity.this, OnboardingUsernameActivity.class); 33 | } 34 | startActivity(intent); 35 | finish(); 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/activities/OnboardingUsernameActivity.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.res.Resources; 6 | import android.graphics.Color; 7 | import android.os.Bundle; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.text.Editable; 10 | import android.text.TextWatcher; 11 | import android.view.View; 12 | import android.widget.EditText; 13 | import android.widget.TextView; 14 | 15 | import io.left.meshim.R; 16 | import io.left.meshim.models.User; 17 | 18 | 19 | public class OnboardingUsernameActivity extends AppCompatActivity { 20 | public static final int MAX_LENGTH_USERNAME_CHARACTERS = 20; 21 | 22 | private UsernameTextWatcher mValidWatcher; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_onboarding_username); 28 | 29 | configureUsernameWatcher(); 30 | } 31 | 32 | static class UsernameTextWatcher implements TextWatcher { 33 | TextView mCharacterCount; 34 | TextView mErrorText; 35 | Resources resources; 36 | boolean mIsUsernameValid = false; 37 | 38 | boolean getIsUsernameValid() { 39 | return mIsUsernameValid; 40 | } 41 | 42 | UsernameTextWatcher(TextView characterCount, TextView errorText, Context context) { 43 | this.mCharacterCount = characterCount; 44 | this.mErrorText = errorText; 45 | resources = context.getResources(); 46 | } 47 | 48 | @Override 49 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 50 | 51 | @Override 52 | public void onTextChanged(CharSequence s, int start, int before, int count) { 53 | String message = resources.getString(R.string.username_length, s.length()); 54 | mCharacterCount.setText(message); 55 | } 56 | 57 | @Override 58 | public void afterTextChanged(Editable s) { 59 | if (s.length() > MAX_LENGTH_USERNAME_CHARACTERS) { 60 | mErrorText.setText(R.string.username_warning_message_length); 61 | mErrorText.setTextColor(Color.RED); 62 | mIsUsernameValid = false; 63 | } else if (s.length() < 1) { 64 | mErrorText.setText(R.string.username_warning_message_empty); 65 | mErrorText.setTextColor(Color.RED); 66 | mIsUsernameValid = false; 67 | } else { 68 | mErrorText.setText(R.string.username_sub_prompt); 69 | mErrorText.setTextColor(Color.BLACK); 70 | mIsUsernameValid = true; 71 | } 72 | } 73 | }; 74 | 75 | 76 | /** 77 | * Creates a User profile when the user presses the save button. 78 | * @param view button that calls the method 79 | */ 80 | public void saveUsername(View view) { 81 | EditText userText = findViewById(R.id.onboarding_username_text_edit); 82 | String userName = userText.getText().toString(); 83 | if (mValidWatcher.getIsUsernameValid()) { 84 | User user = new User(); 85 | user.setUsername(userName); 86 | //set a default avatar 87 | user.setAvatar(R.mipmap.avatar2); 88 | user.save(OnboardingUsernameActivity.this); 89 | 90 | Intent intent = new Intent(OnboardingUsernameActivity.this, ChooseAvatarActivity.class); 91 | intent.setAction(ChooseAvatarActivity.ONBOARDING_ACTION); 92 | startActivity(intent); 93 | 94 | finish(); 95 | } 96 | } 97 | 98 | /** 99 | * Checks for valid usernames. 100 | */ 101 | private void configureUsernameWatcher() { 102 | TextView characterCount = findViewById(R.id.onboarding_username_character_count_text); 103 | TextView errorText = findViewById(R.id.onboarding_username_error_text); 104 | mValidWatcher = new UsernameTextWatcher(characterCount, errorText, this); 105 | 106 | EditText editText = findViewById(R.id.onboarding_username_text_edit); 107 | editText.addTextChangedListener(mValidWatcher); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/activities/ServiceConnectedActivity.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.activities; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Intent; 5 | import android.content.ServiceConnection; 6 | import android.os.Bundle; 7 | import android.os.DeadObjectException; 8 | import android.os.IBinder; 9 | import android.os.RemoteException; 10 | import android.support.v7.app.AppCompatActivity; 11 | 12 | import io.left.meshim.services.IMeshIMService; 13 | import io.left.meshim.services.MeshIMService; 14 | 15 | /** 16 | * Abstract parent class to all Acitivies that need to connect to {@link MeshIMService}. 17 | */ 18 | public abstract class ServiceConnectedActivity extends AppCompatActivity { 19 | // Reference to AIDL interface of app service. 20 | IMeshIMService mService = null; 21 | 22 | // Handles connecting to service. Registers `mCallback` with the service when the 23 | // mConnection is successful. 24 | ServiceConnection mConnection = new ServiceConnection() { 25 | // Called when the mConnection with the service is established 26 | public void onServiceConnected(ComponentName className, IBinder service) { 27 | mService = IMeshIMService.Stub.asInterface(service); 28 | hideService(); 29 | } 30 | 31 | // Called when the mConnection with the service disconnects unexpectedly 32 | public void onServiceDisconnected(ComponentName className) { 33 | mService = null; 34 | } 35 | }; 36 | 37 | // Implementation of AIDL interface. 38 | IActivity.Stub mCallback = new IActivity.Stub() { 39 | @Override 40 | public void updateInterface() throws RemoteException { 41 | ServiceConnectedActivity.this.updateInterface(); 42 | } 43 | }; 44 | 45 | /** 46 | * Connects to service when activity is created. 47 | */ 48 | @Override 49 | protected void onCreate(Bundle savedState) { 50 | super.onCreate(savedState); 51 | connectToService(); 52 | } 53 | 54 | /** 55 | * Hide service notification when activity starts. 56 | */ 57 | @Override 58 | protected void onStart() { 59 | super.onStart(); 60 | hideService(); 61 | } 62 | 63 | /** 64 | * Show service notification and unbind when activity stops. 65 | */ 66 | @Override 67 | protected void onStop() { 68 | super.onStop(); 69 | showService(); 70 | if (mService != null) { 71 | unbindService(mConnection); 72 | mService = null; 73 | } 74 | } 75 | 76 | /** 77 | * To be called when the service connection has broken (e.g. an AIDL call has failed with a 78 | * {@link DeadObjectException}. Disconnects and reconnects to the service. 79 | */ 80 | public void reconnectToService() { 81 | showService(); 82 | unbindService(mConnection); 83 | connectToService(); 84 | } 85 | 86 | /** 87 | * Binds to and starts service. 88 | */ 89 | private void connectToService() { 90 | Intent serviceIntent = new Intent(this, MeshIMService.class); 91 | bindService(serviceIntent, mConnection, BIND_AUTO_CREATE); 92 | startService(serviceIntent); 93 | } 94 | 95 | /** 96 | * Set service to run in foreground mode. 97 | */ 98 | private void showService() { 99 | if (mService != null) { 100 | try { 101 | mService.setForeground(true); 102 | } catch (RemoteException ignored) { 103 | // As we are disconnecting, this isn't our problem. 104 | // When we need the service again we will restart it if it doesn't exist. 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Set service to run in background mode, and register this activity's callback to 111 | * receive updates from the service. 112 | */ 113 | private void hideService() { 114 | if (mService != null) { 115 | try { 116 | mService.registerActivityCallback(mCallback); 117 | mService.setForeground(false); 118 | } catch (RemoteException e) { 119 | // If the connection has died, attempt to reconnect, otherwise ignore it. 120 | if (e instanceof DeadObjectException) { 121 | reconnectToService(); 122 | } 123 | } 124 | } else { 125 | connectToService(); 126 | } 127 | } 128 | 129 | /** 130 | * Called by the {@link MeshIMService} when the state of the mesh has changed, and the UI needs 131 | * to re-draw. 132 | */ 133 | abstract void updateInterface(); 134 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/adapters/ConversationListAdapter.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.adapters; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.graphics.Typeface; 6 | import android.os.DeadObjectException; 7 | import android.os.RemoteException; 8 | import android.support.annotation.NonNull; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ArrayAdapter; 12 | import android.widget.ImageView; 13 | import android.widget.TextView; 14 | 15 | import io.left.meshim.R; 16 | import io.left.meshim.activities.MainActivity; 17 | import io.left.meshim.models.ConversationSummary; 18 | import io.left.meshim.models.Message; 19 | import io.left.meshim.services.IMeshIMService; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * Adapter that fetches conversations from the app service to populate the list of stored 26 | * conversations in {@link MainActivity}. 27 | */ 28 | public class ConversationListAdapter extends ArrayAdapter { 29 | // Used to inflate views for the list. 30 | private Context mContext; 31 | 32 | /** 33 | * Stores context so we can inflate views. 34 | * @param context context of activity 35 | * @param conversations list to manage 36 | */ 37 | public ConversationListAdapter(Context context, ArrayList conversations) { 38 | super(context, R.layout.list_item_online_user, conversations); 39 | this.mContext = context; 40 | } 41 | 42 | /** 43 | * Updates the list of conversation summaries. 44 | * @param service service connection to fetch users from 45 | * @throws DeadObjectException if service connection dies unexpectedly 46 | */ 47 | public void updateList(IMeshIMService service) throws DeadObjectException { 48 | if (service == null) { 49 | return; 50 | } 51 | try { 52 | List query = service.fetchConversationSummaries(); 53 | this.clear(); 54 | this.addAll(query); 55 | } catch (RemoteException e) { 56 | // If the connection has died, propagate error, otherwise ignore it. 57 | if (e instanceof DeadObjectException) { 58 | throw (DeadObjectException) e; 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * {@inheritDoc}. 65 | */ 66 | @Override 67 | @NonNull 68 | public View getView(int position, View convertView, @NonNull ViewGroup parent) { 69 | View v = View.inflate(mContext, R.layout.list_item_conversation, null); 70 | 71 | ConversationSummary conversationSummary = this.getItem(position); 72 | if (conversationSummary != null) { 73 | ImageView userAvatar = v.findViewById(R.id.conversation_avatar); 74 | userAvatar.setImageResource(conversationSummary.avatar); 75 | TextView userName = v.findViewById(R.id.conversation_username); 76 | userName.setText(conversationSummary.username); 77 | TextView newMessage = v.findViewById(R.id.conversation_newest_message_text); 78 | newMessage.setText(conversationSummary.messageText); 79 | TextView newMessageBadge = v.findViewById(R.id.conversation_notification_badge); 80 | if (!conversationSummary.isRead) { 81 | //show how many unread messages there are. 82 | newMessageBadge.setVisibility(View.VISIBLE); 83 | newMessage.setTypeface(null, Typeface.BOLD); 84 | newMessage.setTextColor(Color.BLACK); 85 | newMessageBadge.setText(conversationSummary.unreadMessages + ""); 86 | } else { 87 | newMessageBadge.setVisibility(View.INVISIBLE); 88 | newMessage.setTypeface(null, Typeface.NORMAL); 89 | } 90 | TextView time = v.findViewById(R.id.conversation_newest_message_time); 91 | time.setText(Message.formateDate(conversationSummary.messageTime)); 92 | } 93 | 94 | return v; 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/adapters/OnlineUserListAdapter.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.adapters; 2 | 3 | import android.content.Context; 4 | import android.os.DeadObjectException; 5 | import android.os.RemoteException; 6 | import android.support.annotation.NonNull; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ArrayAdapter; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import io.left.meshim.R; 14 | import io.left.meshim.activities.MainActivity; 15 | import io.left.meshim.models.User; 16 | import io.left.meshim.services.IMeshIMService; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * Adapter that fetches online users from the app service to populate the list of online users in 23 | * {@link MainActivity}. 24 | */ 25 | public class OnlineUserListAdapter extends ArrayAdapter { 26 | // Used to inflate views for the list. 27 | private Context mContext; 28 | 29 | // Reference to user list. Used for updating from service. 30 | private ArrayList mUserList; 31 | 32 | /** 33 | * Stores context so we can inflate views. 34 | * @param context context of activity 35 | * @param userList list to manage 36 | */ 37 | public OnlineUserListAdapter(Context context, ArrayList userList) { 38 | super(context, R.layout.list_item_online_user, userList); 39 | this.mContext = context; 40 | this.mUserList = userList; 41 | } 42 | 43 | /** 44 | * Updates the list of users. 45 | * @param service service connection to fetch users from 46 | * @throws DeadObjectException if service connection dies unexpectedly 47 | */ 48 | public void updateList(IMeshIMService service) throws DeadObjectException { 49 | if (service == null) { 50 | return; 51 | } 52 | 53 | try { 54 | List query = service.getOnlineUsers(); 55 | this.clear(); 56 | this.addAll(query); 57 | } catch (RemoteException e) { 58 | // If the connection has died, propagate error, otherwise ignore it. 59 | if (e instanceof DeadObjectException) { 60 | throw (DeadObjectException) e; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * {@inheritDoc}. 67 | */ 68 | @Override 69 | @NonNull 70 | public View getView(int position, View convertView, @NonNull ViewGroup parent) { 71 | View v = View.inflate(mContext, R.layout.list_item_online_user, null); 72 | 73 | // Null-check the user at this position. 74 | User user = this.getItem(position); 75 | if (user != null) { 76 | ImageView userAvatar = v.findViewById(R.id.conversation_avatar); 77 | userAvatar.setImageResource(user.getAvatar()); 78 | TextView userName = v.findViewById(R.id.conversation_username); 79 | userName.setText(user.getUsername()); 80 | } 81 | 82 | return v; 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/database/Converters.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.database; 2 | 3 | import android.arch.persistence.room.TypeConverter; 4 | 5 | import io.left.rightmesh.id.MeshId; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * A collection of {@link TypeConverter}s for use in {@link MeshIMDatabase}. 11 | */ 12 | public class Converters { 13 | /** 14 | * Initializes a {@link MeshId} from bytes stored in SQLite. 15 | * @param bytes representing a UUID 16 | * @return MeshId with UUID specified in parameters 17 | */ 18 | @TypeConverter 19 | public MeshId meshIdFromBytes(byte[] bytes) { 20 | return new MeshId(bytes); 21 | } 22 | 23 | /** 24 | * Converts a {@link MeshId} to a byte array for storage in SQLite. 25 | * @param id MeshId to be converted 26 | * @return converted UUID of provided MeshId 27 | */ 28 | @TypeConverter 29 | public byte[] bytesFromMeshId(MeshId id) { 30 | return id.getRawMeshId(); 31 | } 32 | 33 | /** 34 | * Initializes a {@link Date} from a long stored in SQLite. 35 | * @param l representing UNIX time in milliseconds 36 | * @return initialized Date 37 | */ 38 | @TypeConverter 39 | public Date dateFromLong(long l) { 40 | return new Date(l); 41 | } 42 | 43 | /** 44 | * Returns the UNIX timestamp from a {@link Date} for storage in SQLite. 45 | * @param date date to get time from 46 | * @return UNIX timestamp 47 | */ 48 | @TypeConverter 49 | public long longFromDate(Date date) { 50 | return date.getTime(); 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/database/MeshIMDao.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.database; 2 | 3 | import android.arch.persistence.room.Dao; 4 | import android.arch.persistence.room.Insert; 5 | import android.arch.persistence.room.Query; 6 | import android.arch.persistence.room.Update; 7 | import android.util.SparseArray; 8 | 9 | import io.left.meshim.activities.MainActivity; 10 | import io.left.meshim.adapters.ConversationListAdapter; 11 | import io.left.meshim.models.ConversationSummary; 12 | import io.left.meshim.models.MeshIdTuple; 13 | import io.left.meshim.models.Message; 14 | import io.left.meshim.models.User; 15 | import io.left.rightmesh.id.MeshId; 16 | 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | /** 21 | * A collection of queries for accessing data types for meshIM. 22 | */ 23 | @Dao 24 | public abstract class MeshIMDao { 25 | @Insert() 26 | public abstract void insertUsers(User... users); 27 | 28 | @Update 29 | public abstract void updateUsers(User... users); 30 | 31 | @Query("SELECT * FROM Users") 32 | public abstract User[] fetchAllUsers(); 33 | 34 | @Query("SELECT UserId, MeshId FROM Users WHERE MeshId = :meshId") 35 | public abstract MeshIdTuple fetchMeshIdTupleByMeshId(MeshId meshId); 36 | 37 | @Query("SELECT * FROM Users WHERE UserId = :id") 38 | public abstract User fetchUserById(int id); 39 | 40 | @Query("SELECT * FROM Users WHERE MeshId = :meshId") 41 | public abstract User fetchUserByMeshId(MeshId meshId); 42 | 43 | @Query("UPDATE Messages SET IsRead = 1 WHERE MessageId=:messageId ") 44 | public abstract void updateMessageIsRead(int messageId); 45 | 46 | /** 47 | * inserts the messages into the database. 48 | * @param messages messages needed to be inserted into the database. 49 | * @return the row id that was inserted. 50 | */ 51 | @Insert() 52 | public abstract long[]insertMessages(Message... messages); 53 | 54 | @Query("SELECT * FROM Messages WHERE SenderId IN (:userIds) AND RecipientId IN (:userIds)" 55 | + " ORDER BY Timestamp ASC") 56 | public abstract Message[] fetchMessagesBetweenUsers(int... userIds); 57 | 58 | @Query("UPDATE Messages SET IsDelivered = 1 WHERE MessageId=:messageId ") 59 | public abstract void updateMessageIsDelivered(int messageId); 60 | 61 | /** 62 | * This query is used by {@link ConversationListAdapter}. That adapter 63 | * populates a tab in {@link MainActivity} which shows a list of 64 | * conversations that have been had with other meshIM users in the past. 65 | * 66 | *

67 | * This query returns a list of {@link ConversationSummary}s, which contain the four fields 68 | * needed to populate a list item in that list: the username and avatar for the user the 69 | * conversation is with, and the message and timestamp of the most recent message exchanged 70 | * with that user. 71 | *

72 | * 73 | *

74 | * This first thing this query does is create a subquery, which shrinks the Messages table 75 | * down to just the most recent message exchanged with each person a message has been 76 | * exchanged with. This is achieved by creating `PeerId`, which maps to a `User.UserId` 77 | * of a user a message has been exchanged with. `PeerId` is created by shrinking each 78 | * `RecipientId`/`SenderId` pairdown to just the UserId that isn't this device's user. Since 79 | * the latter is always 1, the other user's Id will always be greater in value, so we use 80 | * `max(RecipientId, SenderId)` to acquire the foreign party in the pair. Finally, we use 81 | * `MAX(Timestamp) AS Timestamp` and `GROUP BY PeerId` to filter the table down to the most 82 | * recent message for each peer. 83 | *

84 | * 85 | *

86 | * With the messages sorted, we do an inner join on the Users table where PeerId = UserId in 87 | * order to get the information for the user we've had the conversation with. We go in this 88 | * direction because we will not have exchanged a message with every user in the 89 | * Users table. 90 | *

91 | * @return a summary of every conversation the device's user has started 92 | */ 93 | @Query("SELECT Username, Avatar, Contents, Timestamp, PeerId, IsRead,IsDelivered, UnreadMessages " 94 | + "FROM (" 95 | + "SELECT max(RecipientId, SenderId) AS PeerId, Contents,IsRead,IsDelivered, " 96 | + "SUM(CASE WHEN IsRead =0 THEN 1 ELSE 0 END) AS UnreadMessages , " 97 | + "MAX(Timestamp) AS Timestamp FROM Messages GROUP BY PeerId" 98 | + ") INNER JOIN Users ON PeerId = UserId " 99 | + "ORDER BY Timestamp DESC" 100 | ) 101 | public abstract ConversationSummary[] fetchConversationSummaries(); 102 | 103 | /** 104 | * Retrieve the list of messages exchanged between this device and the supplied user. 105 | * @param user user to get messages exchanged with 106 | * @return list of messages exchanged with the supplied user 107 | */ 108 | public List fetchMessagesForUser(User user) { 109 | // Fetch device's user 110 | User deviceUser = this.fetchUserById(User.DEVICE_USER_ID); 111 | 112 | // Retrieve messages from database. 113 | Message[] messages = this.fetchMessagesBetweenUsers(deviceUser.id, user.id); 114 | 115 | // Make User classes easier to find by their id. 116 | SparseArray idUserMap = new SparseArray<>(); 117 | idUserMap.put(deviceUser.id, deviceUser); 118 | idUserMap.put(user.id, user); 119 | 120 | // Populate messages with actual User classes. 121 | for (Message m : messages) { 122 | //marks all the messages loading into chat activity as read. 123 | this.updateMessageIsRead(m.id); 124 | m.setSender(idUserMap.get(m.senderId)); 125 | m.setRecipient(idUserMap.get(m.recipientId)); 126 | } 127 | 128 | return Arrays.asList(messages); 129 | } 130 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/database/MeshIMDatabase.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.database; 2 | 3 | import android.arch.persistence.room.Database; 4 | import android.arch.persistence.room.RoomDatabase; 5 | import android.arch.persistence.room.TypeConverters; 6 | 7 | import io.left.meshim.models.Message; 8 | import io.left.meshim.models.User; 9 | 10 | /** 11 | * Manages versioning and exposed queries for the database for meshIM. 12 | */ 13 | @Database(entities = {User.class, Message.class}, version = 6) 14 | @TypeConverters({Converters.class}) 15 | public abstract class MeshIMDatabase extends RoomDatabase { 16 | public abstract MeshIMDao meshIMDao(); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/database/Migrations.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.database; 2 | 3 | import android.arch.persistence.db.SupportSQLiteDatabase; 4 | import android.arch.persistence.room.migration.Migration; 5 | 6 | //CHECKSTYLE IGNORE LineLengthCheck 7 | public class Migrations { 8 | public static final Migration MIGRATION_1_2 = new Migration(1, 2) { 9 | @Override 10 | public void migrate(SupportSQLiteDatabase database) { 11 | database.execSQL("ALTER TABLE Users RENAME TO OldUsers"); 12 | database.execSQL("CREATE TABLE `Users` (`UserID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `MeshID` BLOB, `Username` TEXT, `Avatar` INTEGER NOT NULL)"); 13 | database.execSQL("CREATE UNIQUE INDEX `index_Users_UserID_MeshID` ON `Users` (`UserID`, `MeshID`)"); 14 | database.execSQL("INSERT INTO Users(UserID, MeshID, Username, Avatar) SELECT UserID, UserMeshID, UserName, UserAvatar FROM OldUsers"); 15 | database.execSQL("DROP TABLE OldUsers"); 16 | } 17 | }; 18 | 19 | public static final Migration MIGRATION_2_3 = new Migration(2, 3) { 20 | @Override 21 | public void migrate(SupportSQLiteDatabase database) { 22 | database.execSQL("CREATE TABLE IF NOT EXISTS `Messages` (`MessageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `Contents` TEXT, `Timestamp` INTEGER, `SenderID` INTEGER NOT NULL, `RecipientID` INTEGER NOT NULL, `SentFromDevice` INTEGER NOT NULL, FOREIGN KEY(`SenderID`) REFERENCES `Users`(`UserID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`RecipientID`) REFERENCES `Users`(`UserID`) ON UPDATE NO ACTION ON DELETE NO ACTION )"); 23 | database.execSQL("CREATE INDEX `index_Messages_RecipientID` ON `Messages` (`RecipientID`)"); 24 | database.execSQL("CREATE INDEX `index_Messages_SenderID` ON `Messages` (`SenderID`)"); 25 | } 26 | }; 27 | public static final Migration MIGRATION_3_4 = new Migration(3, 4) { 28 | @Override 29 | public void migrate(SupportSQLiteDatabase database) { 30 | database.execSQL("ALTER TABLE Messages " 31 | + " ADD COLUMN IsRead INTEGER NOT NULL DEFAULT 1"); 32 | } 33 | 34 | }; 35 | public static final Migration MIGRATION_4_5 = new Migration(4, 5) { 36 | @Override 37 | public void migrate(SupportSQLiteDatabase database) { 38 | database.execSQL("ALTER TABLE Messages " 39 | + " ADD COLUMN IsDelivered INTEGER NOT NULL DEFAULT 1"); 40 | } 41 | }; 42 | 43 | public static final Migration MIGRATION_5_6 = new Migration(5, 6) { 44 | @Override 45 | public void migrate(SupportSQLiteDatabase database) { 46 | database.execSQL("ALTER TABLE Users RENAME TO OldUsers"); 47 | database.execSQL("CREATE TABLE `Users` (`UserId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `MeshId` BLOB, `Username` TEXT, `Avatar` INTEGER NOT NULL)"); 48 | database.execSQL("DROP INDEX `index_Users_UserID_MeshID`"); 49 | database.execSQL("CREATE UNIQUE INDEX `index_Users_UserId_MeshId` ON `Users` (`UserId`, `MeshId`)"); 50 | database.execSQL("INSERT INTO Users(UserId, MeshId, Username, Avatar) SELECT UserID, MeshID, Username, Avatar FROM OldUsers"); 51 | database.execSQL("DROP TABLE OldUsers"); 52 | 53 | database.execSQL("ALTER TABLE Messages RENAME TO OldMessages"); 54 | database.execSQL("CREATE TABLE IF NOT EXISTS Messages (`MessageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `Contents` TEXT, `Timestamp` INTEGER, `SenderId` INTEGER NOT NULL, `RecipientId` INTEGER NOT NULL, `SentFromDevice` INTEGER NOT NULL, `IsRead` INTEGER NOT NULL, `IsDelivered` INTEGER NOT NULL, FOREIGN KEY(`SenderId`) REFERENCES `Users`(`UserId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`RecipientId`) REFERENCES `Users`(`UserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )"); 55 | database.execSQL("DROP INDEX `index_Messages_RecipientId`"); 56 | database.execSQL("DROP INDEX `index_Messages_SenderId`"); 57 | database.execSQL("CREATE INDEX `index_Messages_RecipientId` ON `Messages` (`RecipientId`)"); 58 | database.execSQL("CREATE INDEX `index_Messages_SenderId` ON `Messages` (`SenderId`)"); 59 | database.execSQL("INSERT INTO Messages(MessageId, Contents, Timestamp, SenderId, RecipientId, SentFromDevice, IsRead, IsDelivered) SELECT MessageID, Contents, Timestamp, SenderID, RecipientID, SentFromDevice, IsRead, IsDelivered FROM OldMessages"); 60 | database.execSQL("DROP TABLE OldMessages"); 61 | } 62 | }; 63 | public static final Migration[] ALL_MIGRATIONS = {MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, 64 | MIGRATION_4_5, MIGRATION_5_6 }; 65 | } 66 | //CHECKSTYLE END IGNORE LineLengthCheck -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/models/ConversationSummary.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | 3 | import android.arch.persistence.room.ColumnInfo; 4 | import android.arch.persistence.room.Ignore; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import io.left.meshim.adapters.ConversationListAdapter; 9 | import io.left.meshim.database.MeshIMDao; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * A class that holds the information from {@link MeshIMDao#fetchConversationSummaries()} and is 15 | * used to populate {@link ConversationListAdapter}. 16 | */ 17 | public class ConversationSummary implements Parcelable { 18 | @ColumnInfo(name = "Username") 19 | public String username; 20 | 21 | @ColumnInfo(name = "Avatar") 22 | public int avatar; 23 | 24 | @ColumnInfo(name = "Contents") 25 | public String messageText; 26 | 27 | @ColumnInfo(name = "Timestamp") 28 | public Date messageTime; 29 | 30 | @ColumnInfo(name = "PeerId") 31 | public int peerId; 32 | 33 | @ColumnInfo(name = "IsRead") 34 | public boolean isRead; 35 | 36 | @ColumnInfo(name = "UnreadMessages") 37 | public int unreadMessages; 38 | 39 | @ColumnInfo(name = "IsDelivered") 40 | public boolean isDelivered; 41 | /** 42 | * General purpose setter-constructor used by Room. 43 | * 44 | * @param username username of user conversation is with 45 | * @param avatar avatar of user conversation is with 46 | * @param messageText contents of most recent message in conversation 47 | * @param messageTime time of most recent message in conversation 48 | * @param isRead a boolean to indicate if the last message was read 49 | * @param unreadMessages total number of unread message 50 | */ 51 | public ConversationSummary(String username, int avatar, String messageText, 52 | Date messageTime, boolean isRead, int unreadMessages) { 53 | this.username = username; 54 | this.avatar = avatar; 55 | this.messageText = messageText; 56 | this.messageTime = messageTime; 57 | this.isRead = isRead; 58 | this.unreadMessages = unreadMessages; 59 | } 60 | 61 | /** 62 | * Parcelable constructor. 63 | * @param in parcel to parse 64 | */ 65 | @Ignore 66 | protected ConversationSummary(Parcel in) { 67 | username = in.readString(); 68 | avatar = in.readInt(); 69 | messageText = in.readString(); 70 | messageTime = new Date(in.readLong()); 71 | peerId = in.readInt(); 72 | this.isRead = (in.readInt() == 1); 73 | this.unreadMessages = in.readInt(); 74 | this.isDelivered =(in.readInt()==1); 75 | } 76 | 77 | // Auto-generated Parcelable stuff. 78 | public static final Creator CREATOR = new Creator() { 79 | @Override 80 | public ConversationSummary createFromParcel(Parcel in) { 81 | return new ConversationSummary(in); 82 | } 83 | 84 | @Override 85 | public ConversationSummary[] newArray(int size) { 86 | return new ConversationSummary[size]; 87 | } 88 | }; 89 | 90 | /** 91 | * {@inheritDoc}. 92 | */ 93 | @Override 94 | public int describeContents() { 95 | return 0; 96 | } 97 | 98 | 99 | /** 100 | * Writes the class to a {@link Parcel}. 101 | * @param dest parcel to write to 102 | * @param flags ignored flags 103 | */ 104 | @Override 105 | public void writeToParcel(Parcel dest, int flags) { 106 | dest.writeString(username); 107 | dest.writeInt(avatar); 108 | dest.writeString(messageText); 109 | dest.writeLong(messageTime.getTime()); 110 | dest.writeInt(peerId); 111 | dest.writeInt(isRead ? 1 : 0); 112 | dest.writeInt(unreadMessages); 113 | dest.writeInt(isDelivered ? 1 : 0); 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/models/MeshIdTuple.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | 3 | import android.arch.persistence.room.ColumnInfo; 4 | 5 | import io.left.rightmesh.id.MeshId; 6 | 7 | /** 8 | * Tuple for querying if a MeshId has been seen before. 9 | */ 10 | public class MeshIdTuple { 11 | @ColumnInfo(name = "UserId") 12 | public int id; 13 | 14 | @ColumnInfo(name = "MeshId") 15 | public MeshId meshId; 16 | } -------------------------------------------------------------------------------- /app/src/main/java/io/left/meshim/models/Settings.java: -------------------------------------------------------------------------------- 1 | package io.left.meshim.models; 2 | 3 | import static android.content.Context.MODE_MULTI_PROCESS; 4 | import static android.content.Context.MODE_PRIVATE; 5 | import static io.left.meshim.BuildConfig.APPLICATION_ID; 6 | 7 | import android.content.Context; 8 | import android.content.SharedPreferences; 9 | import android.os.Build; 10 | 11 | import com.google.gson.Gson; 12 | import com.google.gson.reflect.TypeToken; 13 | 14 | import java.lang.reflect.Type; 15 | 16 | /** 17 | * Object that stores application settings. 18 | */ 19 | public class Settings { 20 | // Used in shared preference to store / load data 21 | private static final String SAVE_VERSION = "SettingSaveVersion_v1"; 22 | 23 | // Needed for older phones in order for SharedPreferences to cross the IPC barrier. 24 | private static final int SAVE_MODE 25 | = (Build.VERSION.SDK_INT > 23) ? MODE_PRIVATE : MODE_MULTI_PROCESS; 26 | 27 | private boolean showNotifications; 28 | 29 | public void setShowNotifications(boolean showNotifications) { 30 | this.showNotifications = showNotifications; 31 | } 32 | 33 | public boolean isShowNotifications() { 34 | return showNotifications; 35 | } 36 | 37 | public Settings() { 38 | this(true); 39 | } 40 | 41 | public Settings(boolean showNotification) { 42 | this.showNotifications = showNotification; 43 | } 44 | 45 | /** 46 | * Attempts to load the stored {@link Settings} from {@link SharedPreferences}. 47 | * 48 | * @param context to load {@link SharedPreferences} from. 49 | * @return instance loaded from disk, or null 50 | */ 51 | public static Settings fromDisk(Context context) { 52 | Settings temp = new Settings(); 53 | if (!temp.load(context)) { 54 | return null; 55 | } 56 | return temp; 57 | } 58 | 59 | /** 60 | * Loads settings data from shared preferences if it exist. 61 | * 62 | * @param context context to load shared preferences with 63 | * @return true if function was able to load else false 64 | */ 65 | public boolean load(Context context) { 66 | SharedPreferences preferences = context.getSharedPreferences(APPLICATION_ID, SAVE_MODE); 67 | Gson gson = new Gson(); 68 | String userSetting = preferences.getString(SAVE_VERSION, null); 69 | Type type = new TypeToken(){}.getType(); 70 | Settings temp = gson.fromJson(userSetting, type); 71 | if (temp == null) { 72 | return false; 73 | } else { 74 | this.setShowNotifications(temp.isShowNotifications()); 75 | } 76 | return true; 77 | } 78 | 79 | /** 80 | * Saves the setting data to shared preferences. 81 | * 82 | * @param context context to load shared preferences with 83 | */ 84 | public void save(Context context) { 85 | SharedPreferences preferences = context.getSharedPreferences(APPLICATION_ID, SAVE_MODE); 86 | SharedPreferences.Editor editor = preferences.edit(); 87 | Gson gsonModel = new Gson(); 88 | String savemodel = gsonModel.toJson(this); 89 | editor.putString(SAVE_VERSION, savemodel); 90 | editor.commit(); 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/proto/MeshIMMessages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package protobuf; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | enum MessageType { 7 | PEER_UPDATE = 0; 8 | MESSAGE = 1; 9 | } 10 | 11 | message MeshIMMessage { 12 | MessageType messageType = 1; 13 | PeerUpdate peerUpdate = 2; 14 | Message message = 3; 15 | } 16 | 17 | message PeerUpdate{ 18 | string userName =1; 19 | int32 avatarId =2; 20 | } 21 | message Message{ 22 | string message =1; 23 | google.protobuf.Timestamp time=2; 24 | } -------------------------------------------------------------------------------- /app/src/main/res/color/tabcolor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_select.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 10 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chat_bubble_rec.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chat_bubble_send.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_account_active.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_all_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_in_range_active.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_message_active.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_corner.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrollbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 10 | 12 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 33 | 34 | 46 | 47 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_choose_avatar.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 35 | 36 | 54 | 55 |