├── .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 | *