thomas commited on
Commit
8ab7dfe
·
1 Parent(s): ba9c5ca

feature(#16): merging other repos(android & extension)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .github/workflows/android-release.yml +130 -0
  3. .github/workflows/android.yml +55 -0
  4. .github/workflows/extension_main.yml +78 -0
  5. Android/.gitignore +17 -0
  6. Android/README.md +48 -0
  7. Android/app/.gitignore +1 -0
  8. Android/app/build.gradle +113 -0
  9. Android/app/proguard-rules.pro +21 -0
  10. Android/app/src/androidTest/java/com/matthaigh27/chatgptwrapper/ExampleInstrumentedTest.kt +26 -0
  11. Android/app/src/main/AndroidManifest.xml +48 -0
  12. Android/app/src/main/gpt_icon-playstore.png +0 -0
  13. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/MyApplication.kt +105 -0
  14. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/activites/HomeActivity.kt +92 -0
  15. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/adapters/ChatAdapter.kt +337 -0
  16. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/MyDatabase.kt +38 -0
  17. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/dao/ContactDao.kt +20 -0
  18. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/dao/ImageDao.kt +20 -0
  19. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/entity/ContactEntity.kt +11 -0
  20. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/entity/ImageEntity.kt +11 -0
  21. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/dialogs/CommonConfirmDialog.kt +74 -0
  22. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/fragments/ChatFragment.kt +888 -0
  23. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/ContactModel.kt +19 -0
  24. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/HelpCommandModel.kt +10 -0
  25. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/HelpPromptModel.kt +25 -0
  26. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/ImagePromptModel.kt +11 -0
  27. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/requestmodels/RequestBodyModel.kt +86 -0
  28. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/requestmodels/RequestTrainContactModel.kt +79 -0
  29. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/viewmodels/ChatMessageModel.kt +18 -0
  30. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/MessageService.kt +32 -0
  31. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/api/HttpClient.kt +117 -0
  32. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/api/HttpRisingInterface.kt +7 -0
  33. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/Constants.java +63 -0
  34. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/ImageHelper.java +44 -0
  35. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/ReqType.kt +10 -0
  36. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/Utils.kt +356 -0
  37. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ContactDetailItem.kt +113 -0
  38. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ContactDetailWidget.kt +82 -0
  39. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/HelpCommandEditText.kt +44 -0
  40. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/HelpPromptWidget.kt +98 -0
  41. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ImagePickerWidget.kt +53 -0
  42. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/SearchContactWidget.kt +62 -0
  43. Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/SmsEditorWidget.kt +77 -0
  44. Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
  45. Android/app/src/main/res/drawable/background_common_rounded_button.xml +6 -0
  46. Android/app/src/main/res/drawable/background_content_top.xml +8 -0
  47. Android/app/src/main/res/drawable/background_control_button_common.xml +6 -0
  48. Android/app/src/main/res/drawable/background_dialog_common_confirm.xml +6 -0
  49. Android/app/src/main/res/drawable/background_feedback_layout.xml +8 -0
  50. Android/app/src/main/res/drawable/background_load_assets_button_cancel.xml +6 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.gif filter=lfs diff=lfs merge=lfs -text
.github/workflows/android-release.yml ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Android Release
2
+ env:
3
+ main_project_module: app
4
+ on:
5
+ push:
6
+ tags:
7
+ - '*'
8
+ workflow_dispatch:
9
+ jobs:
10
+ build:
11
+ permissions: write-all
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ # Set Current Date As Env Variable
17
+ - name: Set current date as env variable
18
+ run: |
19
+ cd Android
20
+ echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
21
+
22
+ # Set Repository Name As Env Variable
23
+ - name: Set repository name as env variable
24
+ run: |
25
+ cd Android
26
+ echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
27
+
28
+ - name: set up JDK 11
29
+ uses: actions/setup-java@v3
30
+ with:
31
+ java-version: '11'
32
+ distribution: 'temurin'
33
+ cache: gradle
34
+
35
+ - name: Create file
36
+ run: |
37
+ cd Android
38
+ cat /home/runner/work/RisingPhone/RisingPhone/app/google-services.json | base64
39
+
40
+ - name: Putting data
41
+ env:
42
+ DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
43
+ run: |
44
+ cd Android
45
+ echo $DATA > /home/runner/work/RisingPhone/RisingPhone/app/google-services.json
46
+
47
+ - name: Grant execute permission for gradlew
48
+ run: |
49
+ cd Android
50
+ chmod +x gradlew
51
+
52
+ # Run Tests Build
53
+ - name: Run gradle tests
54
+ run: |
55
+ cd Android
56
+ ./gradlew test
57
+
58
+ - name: Build with Gradle
59
+ run: |
60
+ cd Android
61
+ ./gradlew build
62
+
63
+ - name: Archive lint results
64
+ if: always()
65
+ uses: actions/upload-artifact@v3
66
+ with:
67
+ name: lint-report
68
+ path: app/build/reports/lint-results-debug.html
69
+
70
+ # Create APK Debug
71
+ - name: Build apk debug project (APK) - ${{ env.main_project_module }} module
72
+ run: |
73
+ cd Android
74
+ ./gradlew assembleDebug
75
+
76
+ # Create APK Release
77
+ - name: Build apk release project (APK) - ${{ env.main_project_module }} module
78
+ run: |
79
+ cd Android
80
+ ./gradlew assemble
81
+
82
+ # Create Bundle AAB Release
83
+ # Noted for main module build [main_project_module]:bundleRelease
84
+ - name: Build app bundle release (AAB) - ${{ env.main_project_module }} module
85
+ run: |
86
+ cd Android
87
+ ./gradlew ${{ env.main_project_module }}:bundleRelease
88
+
89
+ # Upload Artifact Build
90
+ # Noted For Output [main_project_module]/build/outputs/apk/debug/
91
+ - name: Upload APK Debug - ${{ env.repository_name }}
92
+ uses: actions/upload-artifact@v2
93
+ with:
94
+ name: ${{ env.date_today }} - ${{ env.playstore_name }} - ${{ env.repository_name }} - APK(s) debug generated
95
+ path: ${{ env.main_project_module }}/build/outputs/apk/debug/
96
+
97
+ # Noted For Output [main_project_module]/build/outputs/apk/release/
98
+ - name: Upload APK Release - ${{ env.repository_name }}
99
+ uses: actions/upload-artifact@v2
100
+ with:
101
+ name: ${{ env.date_today }} - ${{ env.playstore_name }} - ${{ env.repository_name }} - APK(s) release generated
102
+ path: ${{ env.main_project_module }}/build/outputs/apk/release/
103
+
104
+ # Noted For Output [main_project_module]/build/outputs/bundle/release/
105
+ - name: Upload AAB (App Bundle) Release - ${{ env.repository_name }}
106
+ uses: actions/upload-artifact@v2
107
+ with:
108
+ name: ${{ env.date_today }} - ${{ env.playstore_name }} - ${{ env.repository_name }} - App bundle(s) AAB release generated
109
+ path: ${{ env.main_project_module }}/build/outputs/bundle/release/
110
+
111
+ - name: Create Release
112
+ id: create_release
113
+ uses: actions/create-release@v1
114
+ env:
115
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
116
+ with:
117
+ tag_name: ${{ github.ref }}
118
+ release_name: Release ${{ github.ref }}
119
+ draft: false
120
+ prerelease: false
121
+
122
+ - name: Upload Release Asset
123
+ uses: actions/upload-release-asset@v1
124
+ env:
125
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126
+ with:
127
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
128
+ asset_path: ${{ env.main_project_module }}/build/outputs/apk/debug/app-debug.apk # Update this with the correct APK path
129
+ asset_name: app-release-unsigned.apk
130
+ asset_content_type: application/vnd.android.package-archive
.github/workflows/android.yml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Android CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main", "develop" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+ workflow_dispatch:
9
+ jobs:
10
+ build:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - name: set up JDK 11
17
+ uses: actions/setup-java@v3
18
+ with:
19
+ java-version: '11'
20
+ distribution: 'temurin'
21
+ cache: gradle
22
+
23
+ - name: Create file
24
+ run: |
25
+ cd Android
26
+ cat /home/runner/work/RisingPhone/RisingPhone/app/google-services.json | base64
27
+
28
+ - name: Putting data
29
+ env:
30
+ DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
31
+ run: |
32
+ cd Android
33
+ echo $DATA > /home/runner/work/RisingPhone/RisingPhone/app/google-services.json
34
+
35
+ - name: Grant execute permission for gradlew
36
+ run: |
37
+ cd Android
38
+ chmod +x gradlew
39
+ # Run Tests Build
40
+ - name: Run gradle tests
41
+ run: |
42
+ cd Android
43
+ ./gradlew test
44
+
45
+ - name: Build with Gradle
46
+ run: |
47
+ cd Android
48
+ ./gradlew build
49
+
50
+ - name: Archive lint results
51
+ if: always()
52
+ uses: actions/upload-artifact@v3
53
+ with:
54
+ name: lint-report
55
+ path: app/build/reports/lint-results-debug.html
.github/workflows/extension_main.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Development
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - edited
8
+ - synchronize
9
+ - reopened
10
+ workflow_call:
11
+
12
+ jobs:
13
+ test:
14
+ name: Test application
15
+ runs-on: ubuntu-22.04
16
+ timeout-minutes: 10
17
+ steps:
18
+ - name: "☁️ checkout repository"
19
+ uses: actions/checkout@v2
20
+
21
+ - name: "🔧 setup node"
22
+ uses: actions/setup-node@v2.1.5
23
+ with:
24
+ node-version: 16
25
+
26
+ - name: "🔧 install npm@latest"
27
+ run: |
28
+ cd Extension
29
+ npm i -g npm@latest
30
+
31
+ - name: "📦 install dependencies"
32
+ uses: bahmutov/npm-install@v1
33
+
34
+ - name: "🔍 run tests"
35
+ run: |
36
+ cd Extension
37
+ npm run test --if-present
38
+
39
+ lint:
40
+ name: Code standards
41
+ runs-on: ubuntu-22.04
42
+ timeout-minutes: 10
43
+ steps:
44
+ - name: "☁️ checkout repository"
45
+ uses: actions/checkout@v2
46
+
47
+ - name: "🔧 setup node"
48
+ uses: actions/setup-node@v2.1.5
49
+ with:
50
+ node-version: 16
51
+
52
+ - name: "🔧 install npm@latest"
53
+ run: |
54
+ cd Extension
55
+ npm i -g npm@latest
56
+
57
+ - name: "📦 install dependencies"
58
+ uses: bahmutov/npm-install@v1
59
+
60
+ - name: "🔍 lint code"
61
+ run: |
62
+ cd Extension
63
+ npm run lint --if-present
64
+
65
+ build:
66
+ runs-on: ubuntu-22.04
67
+
68
+ steps:
69
+ - uses: actions/checkout@v3
70
+ - uses: actions/setup-node@v3
71
+ with:
72
+ node-version: 16
73
+ - run: |
74
+ cd Extension
75
+ npm ci
76
+ - run: |
77
+ cd Extension
78
+ npm run build
Android/.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.iml
2
+ .gradle
3
+ /local.properties
4
+ /.idea
5
+ /.idea/caches
6
+ /.idea/libraries
7
+ /.idea/modules.xml
8
+ /.idea/workspace.xml
9
+ /.idea/navEditor.xml
10
+ /.idea/assetWizardSettings.xml
11
+ .DS_Store
12
+ /build
13
+ /captures
14
+ .externalNativeBuild
15
+ .cxx
16
+ local.properties
17
+ /app/google-services.json
Android/README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RisingAndroid
2
+ [![Android CI Develop](https://github.com/ttt246/RisingPhone/actions/workflows/android.yml/badge.svg?branch=develop)](https://github.com/ttt246/RisingPhone/actions/workflows/android.yml)
3
+ [![Android CI](https://github.com/ttt246/RisingPhone/actions/workflows/android.yml/badge.svg?branch=main)](https://github.com/ttt246/RisingPhone/actions/workflows/android.yml)
4
+
5
+ All complex software including operating systems will need to be rewritten from the ground up to take advantage of machine learning. In our OS, a AI will manage all apps via plugins, which can be prompted by the user. Our plugins can run as an openai plugin, or in our backend.
6
+
7
+ ### RisingPhone
8
+
9
+ <p align='center'>
10
+ <img align='center' src='assets/img/desc.png' width='100%' />
11
+ </p>
12
+
13
+ - 📱 Support for mobile devices to manage all apps via plugin as its launcher.
14
+ - 🔗 Multiple API support (Web API for Free and Plus users, GPT-3.5, GPT-4, etc.).
15
+ - 🔍 Integration to all mainstream search engines, and custom queries to support additional sites.
16
+
17
+ ### Features
18
+
19
+ | Title | Description |
20
+ | ------------ | ------------ |
21
+ | General Chat | Users can chat with AI plugins. |
22
+ | Open Browser Automatically | If a user is going to open browser, the app opens browser and search what a user wants automatically |
23
+ | Image Search System | A user can search image on Android local storage |
24
+ | Send SMS | If a user says that send SMS, mobile open SMS editor and a user can send SMS using the editor. |
25
+
26
+ [[Rising Brain](https://github.com/ttt246/RisingBrain)]
27
+ ### Run locally
28
+ - Copy google-services.json into app folder of project
29
+
30
+ ### CI/CD
31
+ - set google-services.json to github secrets
32
+
33
+
34
+ ### Test
35
+ - Unit Test
36
+ - Instrumented Test
37
+
38
+ ### Contributing
39
+
40
+ Please refer to each project's style and contribution guidelines for submitting patches and additions. In general, we follow the "fork-and-pull" Git workflow.
41
+
42
+ 1. **Fork** the repo on GitHub
43
+ 2. **Clone** the project to your own machine
44
+ 3. **Commit** changes to your own branch
45
+ 4. **Push** your work back up to your fork
46
+ 5. Submit a **Pull request** so that we can review your changes
47
+
48
+ NOTE: Be sure to merge the latest from "upstream" before making a pull request!
Android/app/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ /build
Android/app/build.gradle ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ plugins {
2
+ id 'com.android.application'
3
+ id 'kotlin-android'
4
+ id 'com.google.gms.google-services'
5
+ id 'kotlin-kapt'
6
+ }
7
+
8
+ android {
9
+ namespace 'com.matthaigh27.chatgptwrapper'
10
+ compileSdk 33
11
+
12
+ defaultConfig {
13
+ applicationId "com.matthaigh27.chatgptwrapper"
14
+ minSdk 28
15
+ targetSdk 33
16
+ versionCode 7
17
+ versionName "1.5"
18
+
19
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20
+ }
21
+
22
+ buildTypes {
23
+ debug {
24
+ buildConfigField "String", "BASE_URL", "\"https://smartphone.herokuapp.com/\""
25
+ }
26
+ release {
27
+ // Use your desired server address for the release version
28
+ buildConfigField "String", "BASE_URL", "\"https://chatgptphone.herokuapp.com/\""
29
+
30
+ minifyEnabled true
31
+ shrinkResources true
32
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33
+ }
34
+ }
35
+
36
+ buildFeatures {
37
+ viewBinding true
38
+ }
39
+
40
+ compileOptions {
41
+ sourceCompatibility JavaVersion.VERSION_11
42
+ targetCompatibility JavaVersion.VERSION_11
43
+ }
44
+ kotlinOptions {
45
+ jvmTarget = JavaVersion.VERSION_11
46
+ }
47
+
48
+ dataBinding {
49
+ enabled true
50
+ }
51
+ }
52
+
53
+ dependencies {
54
+ // App dependencies
55
+ implementation 'androidx.core:core-ktx:1.9.0'
56
+ implementation 'androidx.appcompat:appcompat:1.6.0'
57
+ implementation 'com.google.android.material:material:1.8.0'
58
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
59
+
60
+ implementation platform('com.google.firebase:firebase-bom:31.4.0')
61
+ implementation 'com.google.android.gms:play-services-gcm:17.0.0'
62
+
63
+ implementation 'com.google.firebase:firebase-messaging'
64
+ implementation 'com.google.firebase:firebase-analytics'
65
+ implementation 'com.google.firebase:firebase-firestore-ktx:24.4.5'
66
+ implementation 'com.google.firebase:firebase-firestore:15.0.0'
67
+
68
+ implementation 'com.google.firebase:firebase-messaging-ktx'
69
+ implementation 'com.google.firebase:firebase-analytics-ktx'
70
+ implementation 'com.firebaseui:firebase-ui-storage:7.2.0'
71
+
72
+ implementation 'com.squareup.okhttp3:okhttp:3.0.1'
73
+ implementation 'com.github.soulqw:CoCo:1.1.2'
74
+ implementation 'com.github.dhaval2404:imagepicker:2.1'
75
+ implementation 'com.google.firebase:firebase-storage-ktx:20.1.0'
76
+ testImplementation 'org.testng:testng:6.9.6'
77
+
78
+ // Testing-only dependencies
79
+ androidTestImplementation 'androidx.test:core:' + rootProject.coreVersion;
80
+ androidTestImplementation 'androidx.test.ext:junit:' + rootProject.extJUnitVersion;
81
+ androidTestImplementation 'androidx.test:runner:' + rootProject.runnerVersion;
82
+
83
+ testImplementation 'junit:junit:4.13.2'
84
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
85
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
86
+
87
+ // UiAutomator Testing
88
+ androidTestImplementation 'androidx.test.uiautomator:uiautomator:' + rootProject.uiAutomatorVersion;
89
+ androidTestImplementation 'org.hamcrest:hamcrest-integration:1.3'
90
+
91
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
92
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
93
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha05"
94
+
95
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
96
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
97
+
98
+ implementation 'com.google.code.gson:gson:2.8.5'
99
+
100
+ implementation "androidx.room:room-runtime:$room_version"
101
+ annotationProcessor "androidx.room:room-compiler:$room_version"
102
+
103
+ // To use room database
104
+ implementation "androidx.room:room-ktx:$room_version"
105
+ kapt "androidx.room:room-compiler:$room_version"
106
+ implementation "androidx.room:room-rxjava2:$room_version"
107
+ implementation "androidx.room:room-rxjava3:$room_version"
108
+ implementation "androidx.room:room-guava:$room_version"
109
+ testImplementation "androidx.room:room-testing:$room_version"
110
+ implementation "androidx.room:room-paging:$room_version"
111
+
112
+ implementation 'de.hdodenhof:circleimageview:3.1.0'
113
+ }
Android/app/proguard-rules.pro ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Add project specific ProGuard rules here.
2
+ # You can control the set of applied configuration files using the
3
+ # proguardFiles setting in build.gradle.
4
+ #
5
+ # For more details, see
6
+ # http://developer.android.com/guide/developing/tools/proguard.html
7
+
8
+ # If your project uses WebView with JS, uncomment the following
9
+ # and specify the fully qualified class name to the JavaScript interface
10
+ # class:
11
+ #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12
+ # public *;
13
+ #}
14
+
15
+ # Uncomment this to preserve the line number information for
16
+ # debugging stack traces.
17
+ #-keepattributes SourceFile,LineNumberTable
18
+
19
+ # If you keep the line number information, uncomment this to
20
+ # hide the original source file name.
21
+ #-renamesourcefileattribute SourceFile
Android/app/src/androidTest/java/com/matthaigh27/chatgptwrapper/ExampleInstrumentedTest.kt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper
2
+
3
+ import androidx.test.platform.app.InstrumentationRegistry
4
+ import androidx.test.ext.junit.runners.AndroidJUnit4
5
+
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+
9
+ import org.junit.Assert.*
10
+
11
+ /**
12
+ * Instrumented test, which will execute on an Android device.
13
+ *
14
+ * See [testing documentation](http://d.android.com/tools/testing).
15
+ */
16
+ @RunWith(AndroidJUnit4::class)
17
+ class
18
+
19
+ ExampleInstrumentedTest {
20
+ @Test
21
+ fun useAppContext() {
22
+ // Context of the app under test.
23
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
24
+ assertEquals("com.matthaigh27.chatgptwrapper", appContext.packageName)
25
+ }
26
+ }
Android/app/src/main/AndroidManifest.xml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ package="com.matthaigh27.chatgptwrapper">
4
+
5
+ <uses-permission android:name="android.permission.INTERNET" />
6
+ <uses-permission android:name="android.permission.MICROPHONE" />
7
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
8
+ <uses-permission android:name="android.permission.LAUNCHER" />
9
+ <uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
10
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
11
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
12
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
13
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
14
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
15
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
16
+ <uses-permission android:name="android.permission.SEND_SMS"/>
17
+ <uses-permission android:name="android.permission.CALL_PHONE" />
18
+
19
+ <uses-feature android:name="android.hardware.audio.low_latency" />
20
+ <uses-feature android:name="android.hardware.audio.pro" />
21
+ <uses-feature android:name="android.hardware.microphone" />
22
+
23
+ <application
24
+ android:name=".MyApplication"
25
+ android:hardwareAccelerated="true"
26
+ android:icon="@mipmap/gpt_icon"
27
+ android:label="@string/app_name"
28
+ android:roundIcon="@mipmap/gpt_icon_round"
29
+ android:supportsRtl="true"
30
+ android:theme="@style/Theme.Design.NoActionBar">
31
+ <activity
32
+ android:name=".activites.HomeActivity"
33
+ android:windowSoftInputMode="adjustResize"
34
+ android:exported="true">
35
+ <intent-filter>
36
+ <action android:name="android.intent.action.MAIN" />
37
+ <category android:name="android.intent.category.LAUNCHER" />
38
+ </intent-filter>
39
+ </activity>
40
+ <service
41
+ android:name=".services.MessageService"
42
+ android:exported="false">
43
+ <intent-filter>
44
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
45
+ </intent-filter>
46
+ </service>
47
+ </application>
48
+ </manifest>
Android/app/src/main/gpt_icon-playstore.png ADDED
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/MyApplication.kt ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.app.Application
6
+ import android.app.NotificationChannel
7
+ import android.app.NotificationManager
8
+ import android.content.pm.PackageManager
9
+ import android.util.Log
10
+ import com.google.android.gms.tasks.OnCompleteListener
11
+ import com.google.firebase.messaging.FirebaseMessaging
12
+ import com.matthaigh27.chatgptwrapper.utils.Constants
13
+ import android.provider.Settings
14
+ import androidx.core.app.ActivityCompat
15
+ import androidx.core.app.NotificationCompat
16
+ import androidx.core.app.NotificationManagerCompat
17
+ import java.util.Random
18
+
19
+ class MyApplication : Application() {
20
+
21
+ private var mFCMToken: String = String()
22
+ private var mUUID: String = String()
23
+ @SuppressLint("HardwareIds")
24
+ override fun onCreate() {
25
+ super.onCreate()
26
+
27
+ initToken()
28
+ // on below line we are getting device id.
29
+ mUUID = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
30
+ appContext = applicationContext as MyApplication
31
+
32
+ Log.v("risingandroid mUUID: ", mUUID)
33
+ Log.v("risingandroid FCMToken: ", mFCMToken)
34
+ }
35
+
36
+ private fun initToken() {
37
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
38
+ if (!task.isSuccessful) {
39
+ Log.w(Constants.TAG, "Fetching FCM registration token failed", task.exception)
40
+ return@OnCompleteListener
41
+ }
42
+
43
+ /**
44
+ * Get new FCM registration token
45
+ */
46
+
47
+ /**
48
+ * Get new FCM registration token
49
+ */
50
+ mFCMToken = task.result
51
+ Log.d(Constants.TAG, mFCMToken)
52
+ })
53
+ }
54
+
55
+ fun getFCMToken(): String {
56
+ return this.mFCMToken
57
+ }
58
+
59
+ fun getUUID(): String {
60
+ return this.mUUID
61
+ }
62
+
63
+ /**
64
+ * this shows system notification with message
65
+ * @param message to be shown with system notification
66
+ */
67
+ fun showNotification(message: String) {
68
+ val notificationId: Int = Random().nextInt()
69
+ val channelId = "chat_message"
70
+
71
+ val builder = NotificationCompat.Builder(this, channelId)
72
+ builder.setSmallIcon(R.drawable.ic_notification)
73
+ builder.setContentTitle(Constants.TAG)
74
+ builder.setContentText(message)
75
+ builder.setStyle(
76
+ NotificationCompat.BigTextStyle().bigText(
77
+ message
78
+ )
79
+ )
80
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
81
+ builder.setAutoCancel(true)
82
+
83
+ val channelName: CharSequence = "Chat Message"
84
+ val channelDescription = "This notification channel is used for chat message notifications"
85
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
86
+ val channel = NotificationChannel(channelId, channelName, importance)
87
+ channel.description = channelDescription
88
+ val notificationManager = getSystemService(
89
+ NotificationManager::class.java
90
+ )
91
+ notificationManager.createNotificationChannel(channel)
92
+ val notificationManagerCompat = NotificationManagerCompat.from(this)
93
+ if (ActivityCompat.checkSelfPermission(
94
+ this, Manifest.permission.POST_NOTIFICATIONS
95
+ ) != PackageManager.PERMISSION_GRANTED
96
+ ) {
97
+ return
98
+ }
99
+ notificationManagerCompat.notify(notificationId, builder.build())
100
+ }
101
+
102
+ companion object {
103
+ lateinit var appContext: MyApplication
104
+ }
105
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/activites/HomeActivity.kt ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.activites
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.os.Bundle
8
+ import android.util.Log
9
+ import androidx.appcompat.app.AlertDialog
10
+ import androidx.appcompat.app.AppCompatActivity
11
+ import com.google.android.material.tabs.TabLayout.TabGravity
12
+ import com.matthaigh27.chatgptwrapper.R
13
+ import com.matthaigh27.chatgptwrapper.dialogs.CommonConfirmDialog
14
+ import com.matthaigh27.chatgptwrapper.fragments.ChatFragment
15
+ import java.io.File
16
+
17
+
18
+ class HomeActivity : AppCompatActivity() {
19
+
20
+ private val PERMISSIONS_REQUEST_CODE = 1
21
+
22
+ private val PERMISSIONS = arrayOf(
23
+ Manifest.permission.SEND_SMS,
24
+ Manifest.permission.READ_CONTACTS,
25
+ Manifest.permission.CALL_PHONE,
26
+ Manifest.permission.READ_EXTERNAL_STORAGE
27
+ )
28
+
29
+ override fun onCreate(savedInstanceState: Bundle?) {
30
+ super.onCreate(savedInstanceState)
31
+ setContentView(R.layout.activity_home)
32
+
33
+ requestPermission()
34
+ }
35
+
36
+ private fun requestPermission() {
37
+ val notGrantedPermissions = PERMISSIONS.filter {
38
+ checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED
39
+ }
40
+
41
+ if (notGrantedPermissions.isNotEmpty()) {
42
+ if (shouldShowRequestPermissionRationale(notGrantedPermissions[0])) {
43
+ // show custom permission rationale
44
+ val confirmDialog = CommonConfirmDialog(this)
45
+ confirmDialog.setMessage("This app requires SMS, Contacts and Phone permissions to function properly. Please grant the necessary permissions.")
46
+ confirmDialog.setOnClickListener(object :
47
+ CommonConfirmDialog.OnConfirmButtonClickListener {
48
+ override fun onPositiveButtonClick() {
49
+ requestPermissions(
50
+ notGrantedPermissions.toTypedArray(), PERMISSIONS_REQUEST_CODE
51
+ )
52
+ }
53
+
54
+ override fun onNegativeButtonClick() {
55
+ finish()
56
+ }
57
+ })
58
+ confirmDialog.show()
59
+ } else {
60
+ requestPermissions(notGrantedPermissions.toTypedArray(), PERMISSIONS_REQUEST_CODE)
61
+ }
62
+ } else {
63
+ // Permissions already granted, navigate to your desired fragment
64
+ navigateToChatFragment()
65
+ }
66
+ }
67
+
68
+ private fun navigateToChatFragment() {
69
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
70
+ fragmentTransaction.replace(R.id.frame_container, ChatFragment()).commit()
71
+ }
72
+
73
+ @SuppressLint("MissingSuperCall")
74
+ override fun onRequestPermissionsResult(
75
+ requestCode: Int, permissions: Array<out String>, grantResults: IntArray
76
+ ) {
77
+ when (requestCode) {
78
+ PERMISSIONS_REQUEST_CODE -> {
79
+ if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
80
+ // Permissions granted, navigate to your desired fragment
81
+ navigateToChatFragment()
82
+ } else {
83
+ requestPermission()
84
+ }
85
+ return
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+
92
+
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/adapters/ChatAdapter.kt ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.adapters
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.Dialog
5
+ import android.content.Context
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.view.LayoutInflater
9
+ import android.view.View
10
+ import android.view.ViewGroup
11
+ import android.widget.FrameLayout
12
+ import android.widget.ImageView
13
+ import android.widget.LinearLayout
14
+ import android.widget.TextView
15
+ import androidx.constraintlayout.widget.ConstraintLayout
16
+ import androidx.recyclerview.widget.RecyclerView
17
+ import com.matthaigh27.chatgptwrapper.R
18
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
19
+ import com.matthaigh27.chatgptwrapper.models.common.HelpPromptModel
20
+ import com.matthaigh27.chatgptwrapper.models.viewmodels.ChatMessageModel
21
+ import com.matthaigh27.chatgptwrapper.utils.Constants.MSG_WIDGET_TYPE_HELP_PRMOPT
22
+ import com.matthaigh27.chatgptwrapper.utils.Constants.MSG_WIDGET_TYPE_SEARCH_CONTACT
23
+ import com.matthaigh27.chatgptwrapper.utils.Constants.MSG_WIDGET_TYPE_SMS
24
+ import com.matthaigh27.chatgptwrapper.utils.ImageHelper
25
+ import com.matthaigh27.chatgptwrapper.utils.Utils
26
+ import com.matthaigh27.chatgptwrapper.widgets.ContactDetailItem
27
+ import com.matthaigh27.chatgptwrapper.widgets.ContactDetailWidget
28
+ import com.matthaigh27.chatgptwrapper.widgets.HelpPromptWidget
29
+ import com.matthaigh27.chatgptwrapper.widgets.SearchContactWidget
30
+ import com.matthaigh27.chatgptwrapper.widgets.SmsEditorWidget
31
+ import org.json.JSONArray
32
+
33
+ class ChatAdapter(list: ArrayList<ChatMessageModel>, context: Context) :
34
+ RecyclerView.Adapter<RecyclerView.ViewHolder>() {
35
+
36
+ private var mChatModelList: ArrayList<ChatMessageModel> = ArrayList()
37
+ private var mContext: Context
38
+
39
+ private var mListener: MessageWidgetListener? = null
40
+ var mOnSMSClickListener: ContactDetailItem.OnSMSClickListener? = null
41
+
42
+ private val feedbackData = arrayOf(
43
+ arrayOf(R.drawable.ic_thumb_up_disable, R.drawable.ic_thumb_down),
44
+ arrayOf(R.drawable.ic_thumb_up_disable, R.drawable.ic_thumb_down_disable),
45
+ arrayOf(R.drawable.ic_thumb_up, R.drawable.ic_thumb_down_disable),
46
+ )
47
+
48
+ init {
49
+ mChatModelList = list
50
+ mContext = context
51
+ }
52
+
53
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
54
+ val context = parent.context
55
+ val inflater = LayoutInflater.from(context)
56
+
57
+ /**
58
+ * Inflate the custom layout and Return a new holder instance
59
+ */
60
+ return if (viewType == 0) {
61
+ SendMessageViewHolder(
62
+ inflater.inflate(
63
+ R.layout.item_container_sent_message, parent, false
64
+ )
65
+ )
66
+ } else {
67
+ ReceiveMessageViewHolder(
68
+ inflater.inflate(
69
+ R.layout.item_container_received_message, parent, false
70
+ )
71
+ )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Involves populating data into the item through holder
77
+ */
78
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
79
+ /**
80
+ * Get the data model based on position
81
+ */
82
+ val index = holder.adapterPosition
83
+ val messageModel: ChatMessageModel = mChatModelList[index]
84
+ if (messageModel.isMe) {
85
+ setSentMessageData(holder as SendMessageViewHolder, messageModel)
86
+ } else {
87
+ setReceiveMessageData(holder as ReceiveMessageViewHolder, messageModel)
88
+ }
89
+ }
90
+
91
+ private fun setSentMessageData(holder: SendMessageViewHolder, messageModel: ChatMessageModel) {
92
+ /**
93
+ * Set item views based on your views and data model
94
+ */
95
+ if (messageModel.message.isEmpty()) {
96
+ holder.textMessage.visibility = View.GONE
97
+ } else {
98
+ holder.textMessage.text = messageModel.message
99
+ holder.textMessage.visibility = View.VISIBLE
100
+ }
101
+
102
+
103
+ if (messageModel.image != null) {
104
+ val radius = mContext.resources.getDimensionPixelSize(R.dimen.chat_message_item_radius)
105
+
106
+ val originBmp = BitmapFactory.decodeByteArray(messageModel.image, 0, messageModel.image!!.size)
107
+ val bmp = ImageHelper.getRoundedCornerBitmap(originBmp, radius)
108
+ holder.imgMessage.visibility = View.VISIBLE
109
+ holder.imgMessage.setImageBitmap(bmp)
110
+ holder.imgMessage.setOnClickListener {
111
+ onImageClick(originBmp)
112
+ }
113
+ } else {
114
+ holder.imgMessage.visibility = View.GONE
115
+ }
116
+
117
+ if (messageModel.isWidget) {
118
+ when (messageModel.widgetType) {
119
+ MSG_WIDGET_TYPE_HELP_PRMOPT -> {
120
+ val model: HelpPromptModel =
121
+ HelpPromptModel.initModelWithString(messageModel.widgetDescription)
122
+ val helpPromptWidget = HelpPromptWidget(mContext, model)
123
+ val helpPromptListener = object : HelpPromptWidget.OnHelpPromptListener {
124
+ override fun onSuccess(prompt: String) {
125
+ mChatModelList[holder.adapterPosition].isWidget = false
126
+ holder.llMessageWidget.visibility = View.GONE
127
+ holder.llMessageWidget.removeAllViews()
128
+ mListener!!.sentHelpPrompt(prompt)
129
+ }
130
+
131
+ override fun onCancel() {
132
+ mChatModelList[holder.adapterPosition].isWidget = false
133
+ holder.llMessageWidget.visibility = View.GONE
134
+ holder.llMessageWidget.removeAllViews()
135
+ mListener!!.canceledHelpPrompt()
136
+ }
137
+ }
138
+ helpPromptWidget.setOnClickListener(helpPromptListener)
139
+ holder.llMessageWidget.addView(helpPromptWidget)
140
+ holder.llMessageWidget.visibility = View.VISIBLE
141
+ }
142
+ }
143
+ } else {
144
+ holder.llMessageWidget.visibility = View.GONE
145
+ }
146
+ }
147
+
148
+ @SuppressLint("UseCompatLoadingForDrawables")
149
+ fun setReceiveMessageData(holder: ReceiveMessageViewHolder, messageModel: ChatMessageModel) {
150
+ /**
151
+ * Set item views based on your views and data model
152
+ */
153
+ if (messageModel.message.isEmpty()) {
154
+ holder.textMessage.visibility = View.GONE
155
+ } else {
156
+ holder.textMessage.text = messageModel.message
157
+ holder.textMessage.visibility = View.VISIBLE
158
+ }
159
+
160
+
161
+ if (messageModel.image != null) {
162
+ val radius = mContext.resources.getDimensionPixelSize(R.dimen.chat_message_item_radius)
163
+
164
+ val originBmp = BitmapFactory.decodeByteArray(messageModel.image, 0, messageModel.image!!.size)
165
+ val bmp = ImageHelper.getRoundedCornerBitmap(originBmp, radius)
166
+ holder.imgMessage.visibility = View.VISIBLE
167
+ holder.imgMessage.setImageBitmap(bmp)
168
+ holder.imgMessage.setOnClickListener {
169
+ onImageClick(originBmp)
170
+ }
171
+ } else {
172
+ holder.imgMessage.visibility = View.GONE
173
+ }
174
+
175
+ holder.llFeedback.visibility = if (messageModel.visibleFeedback) {
176
+ View.VISIBLE
177
+ } else {
178
+ View.GONE
179
+ }
180
+
181
+ setThumb(holder)
182
+
183
+ holder.itemLayout.setOnLongClickListener {
184
+ if (holder.llFeedback.visibility == View.VISIBLE) {
185
+ holder.llFeedback.visibility = View.GONE
186
+ mChatModelList[holder.adapterPosition].visibleFeedback = false
187
+ } else {
188
+ holder.llFeedback.visibility = View.VISIBLE
189
+ mChatModelList[holder.adapterPosition].visibleFeedback = true
190
+ }
191
+ return@setOnLongClickListener true
192
+ }
193
+
194
+ holder.btnThumbUp.setOnClickListener {
195
+ mChatModelList[holder.adapterPosition].feedback = 1
196
+ setThumb(holder)
197
+
198
+ }
199
+
200
+ holder.btnThumbDown.setOnClickListener {
201
+ mChatModelList[holder.adapterPosition].feedback = -1
202
+ setThumb(holder)
203
+ }
204
+
205
+ if (messageModel.isWidget) {
206
+ holder.llContactWidget.removeAllViews()
207
+ when (messageModel.widgetType) {
208
+ MSG_WIDGET_TYPE_SMS -> {
209
+ val smsWidget = SmsEditorWidget(mContext, null)
210
+ if(messageModel.widgetDescription.isNotEmpty()) {
211
+ smsWidget.setToUserName(messageModel.widgetDescription)
212
+ }
213
+ holder.llMessageWidget.addView(smsWidget)
214
+ holder.llMessageWidget.visibility = View.VISIBLE
215
+
216
+ val smsListener = object : SmsEditorWidget.OnClickListener {
217
+ override fun confirmSMS(phonenumber: String, message: String) {
218
+ mChatModelList[holder.adapterPosition].isWidget = false
219
+ holder.llMessageWidget.visibility = View.GONE
220
+ holder.llMessageWidget.removeAllViews()
221
+ mListener!!.sentSMS(phonenumber, message)
222
+ }
223
+
224
+ override fun cancelSMS() {
225
+ mChatModelList[holder.adapterPosition].isWidget = false
226
+ holder.llMessageWidget.visibility = View.GONE
227
+ holder.llMessageWidget.removeAllViews()
228
+ mListener!!.canceledSMS()
229
+ }
230
+ }
231
+
232
+ smsWidget.setOnClickListener(smsListener)
233
+ }
234
+
235
+ MSG_WIDGET_TYPE_SEARCH_CONTACT -> {
236
+ val contacts = Utils.instance.getContacts(mContext)
237
+
238
+ val contactIds = JSONArray(messageModel.widgetDescription)
239
+ for (i in 0 until contactIds.length()) {
240
+ val contactId = contactIds[i].toString()
241
+ val contact = Utils.instance.getContactModelById(contactId, contacts)
242
+
243
+ val searchContactWidget = SearchContactWidget(mContext, contact, null)
244
+ searchContactWidget.mSMSOnClickListener = mOnSMSClickListener
245
+ holder.llContactWidget.addView(searchContactWidget)
246
+ }
247
+ holder.llContactWidget.visibility = View.VISIBLE
248
+ }
249
+ }
250
+ } else {
251
+ holder.llMessageWidget.visibility = View.GONE
252
+ holder.llContactWidget.visibility = View.GONE
253
+ }
254
+ }
255
+
256
+ @SuppressLint("UseCompatLoadingForDrawables")
257
+ fun setThumb(holder: ReceiveMessageViewHolder) {
258
+ holder.btnThumbUp.setImageDrawable(
259
+ mContext.getDrawable(
260
+ feedbackData[mChatModelList[holder.adapterPosition].feedback + 1][0]
261
+ )
262
+ )
263
+ holder.btnThumbDown.setImageDrawable(
264
+ mContext.getDrawable(
265
+ feedbackData[mChatModelList[holder.adapterPosition].feedback + 1][1]
266
+ )
267
+ )
268
+ }
269
+
270
+ /**
271
+ * Returns the total count of items in the list
272
+ */
273
+ override fun getItemCount(): Int {
274
+ return mChatModelList.size
275
+ }
276
+
277
+ override fun getItemViewType(position: Int): Int {
278
+ return if (mChatModelList[position].isMe) 0 else 1
279
+ }
280
+
281
+ private fun onImageClick(bitmap: Bitmap) {
282
+ val dialog = Dialog(mContext)
283
+ dialog.setContentView(R.layout.view_full_image)
284
+ val fullImage = dialog.findViewById(R.id.fullImage) as ImageView
285
+ fullImage.setImageBitmap(bitmap)
286
+ dialog.show()
287
+ }
288
+
289
+ inner class ReceiveMessageViewHolder internal constructor(itemView: View) :
290
+ RecyclerView.ViewHolder(itemView) {
291
+ var textMessage: TextView
292
+ var imgMessage: ImageView
293
+ var llFeedback: LinearLayout
294
+ var btnThumbUp: ImageView
295
+ var btnThumbDown: ImageView
296
+ var itemLayout: ConstraintLayout
297
+ var llMessageWidget: LinearLayout
298
+ var llContactWidget: LinearLayout
299
+
300
+ init {
301
+ textMessage = itemView.findViewById<View>(R.id.textMessage) as TextView
302
+ imgMessage = itemView.findViewById<View>(R.id.imgMessage) as ImageView
303
+ btnThumbUp = itemView.findViewById<View>(R.id.btn_thumb_up) as ImageView
304
+ btnThumbDown = itemView.findViewById<View>(R.id.btn_thumb_down) as ImageView
305
+ llFeedback = itemView.findViewById<View>(R.id.ll_feedback) as LinearLayout
306
+ itemLayout = itemView.findViewById<View>(R.id.cl_receive_message) as ConstraintLayout
307
+ llMessageWidget = itemView.findViewById<View>(R.id.ll_message_widget) as LinearLayout
308
+ llContactWidget = itemView.findViewById<View>(R.id.ll_contacts_widget) as LinearLayout
309
+ }
310
+ }
311
+
312
+ inner class SendMessageViewHolder internal constructor(itemView: View) :
313
+ RecyclerView.ViewHolder(itemView) {
314
+ var textMessage: TextView
315
+ var imgMessage: ImageView
316
+ var itemLayout: ConstraintLayout
317
+ var llMessageWidget: LinearLayout
318
+
319
+ init {
320
+ textMessage = itemView.findViewById<View>(R.id.textMessage) as TextView
321
+ imgMessage = itemView.findViewById<View>(R.id.imgMessage) as ImageView
322
+ itemLayout = itemView.findViewById<View>(R.id.cl_sent_message) as ConstraintLayout
323
+ llMessageWidget = itemView.findViewById<View>(R.id.ll_message_widget) as LinearLayout
324
+ }
325
+ }
326
+
327
+ interface MessageWidgetListener {
328
+ fun sentSMS(phonenumber: String, message: String)
329
+ fun canceledSMS()
330
+ fun sentHelpPrompt(prompt: String)
331
+ fun canceledHelpPrompt()
332
+ }
333
+
334
+ fun setMessageWidgetListener(listener: MessageWidgetListener) {
335
+ mListener = listener
336
+ }
337
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/MyDatabase.kt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.database
2
+
3
+ import android.content.Context
4
+ import androidx.room.Database
5
+ import androidx.room.Room
6
+ import androidx.room.RoomDatabase
7
+ import com.matthaigh27.chatgptwrapper.database.dao.ContactDao
8
+ import com.matthaigh27.chatgptwrapper.database.dao.ImageDao
9
+ import com.matthaigh27.chatgptwrapper.database.entity.ContactEntity
10
+ import com.matthaigh27.chatgptwrapper.database.entity.ImageEntity
11
+
12
+ @Database(entities = [ImageEntity::class, ContactEntity::class], version = 1, exportSchema = false)
13
+ abstract class MyDatabase : RoomDatabase() {
14
+
15
+ abstract fun imageDao(): ImageDao
16
+ abstract fun contactDao(): ContactDao
17
+
18
+ companion object {
19
+ @Volatile
20
+ private var INSTANCE: MyDatabase? = null
21
+
22
+ fun getDatabase(context: Context): MyDatabase {
23
+ val tempInstance = INSTANCE
24
+ if (tempInstance != null) {
25
+ return tempInstance
26
+ }
27
+ synchronized(this) {
28
+ val instance = Room.databaseBuilder(
29
+ context.applicationContext,
30
+ MyDatabase::class.java,
31
+ "risingphone_database"
32
+ ).build()
33
+ INSTANCE = instance
34
+ return instance
35
+ }
36
+ }
37
+ }
38
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/dao/ContactDao.kt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.database.dao
2
+
3
+ import androidx.lifecycle.LiveData
4
+ import androidx.room.*
5
+ import com.matthaigh27.chatgptwrapper.database.entity.ContactEntity
6
+
7
+ @Dao
8
+ interface ContactDao {
9
+ @Insert
10
+ suspend fun insertContact(contact: ContactEntity)
11
+
12
+ @Update
13
+ suspend fun updateContact(contact: ContactEntity)
14
+
15
+ @Delete
16
+ suspend fun deleteContact(contact: ContactEntity)
17
+
18
+ @Query("SELECT * FROM contacts")
19
+ fun getAllContacts(): List<ContactEntity>
20
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/dao/ImageDao.kt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.database.dao
2
+
3
+ import androidx.lifecycle.LiveData
4
+ import androidx.room.*
5
+ import com.matthaigh27.chatgptwrapper.database.entity.ImageEntity
6
+
7
+ @Dao
8
+ interface ImageDao {
9
+ @Insert
10
+ suspend fun insertImage(image: ImageEntity)
11
+
12
+ @Update
13
+ suspend fun updateImage(image: ImageEntity)
14
+
15
+ @Delete
16
+ suspend fun deleteImage(image: ImageEntity)
17
+
18
+ @Query("SELECT * FROM images")
19
+ fun getAllImages(): List<ImageEntity>
20
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/entity/ContactEntity.kt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.database.entity
2
+
3
+ import androidx.room.Entity
4
+ import androidx.room.PrimaryKey
5
+
6
+ @Entity(tableName = "contacts")
7
+ data class ContactEntity (
8
+ @PrimaryKey(autoGenerate = false) val id: String,
9
+ val name: String,
10
+ val phoneNumber: String,
11
+ )
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/database/entity/ImageEntity.kt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.database.entity
2
+
3
+ import androidx.room.Entity
4
+ import androidx.room.PrimaryKey
5
+
6
+ @Entity(tableName = "images")
7
+ data class ImageEntity (
8
+ @PrimaryKey(autoGenerate = true) val id: Int,
9
+ val path: String,
10
+ val name: String,
11
+ )
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/dialogs/CommonConfirmDialog.kt ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.dialogs
2
+
3
+ import android.app.Dialog
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import android.graphics.drawable.ColorDrawable
7
+ import android.os.Bundle
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.Window
11
+ import android.widget.Button
12
+ import android.widget.TextView
13
+ import com.matthaigh27.chatgptwrapper.R
14
+
15
+ class CommonConfirmDialog(context: Context) : Dialog(context), View.OnClickListener {
16
+
17
+ private var mTvMessage: TextView? = null
18
+ private var mMessage: String = ""
19
+ private lateinit var mClickListener: OnConfirmButtonClickListener
20
+
21
+ init {
22
+ setCancelable(false)
23
+ }
24
+
25
+ fun setOnClickListener(listener: OnConfirmButtonClickListener) {
26
+ mClickListener = listener
27
+ }
28
+
29
+ override fun onCreate(savedInstanceState: Bundle?) {
30
+ super.onCreate(savedInstanceState)
31
+
32
+ initView()
33
+ }
34
+
35
+ private fun initView() {
36
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
37
+ setContentView(R.layout.dialog_common_confirm)
38
+
39
+ window!!.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
40
+ window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
41
+
42
+ findViewById<Button>(R.id.btn_ok).setOnClickListener(this)
43
+ findViewById<Button>(R.id.btn_cancel).setOnClickListener(this)
44
+
45
+ mTvMessage = findViewById(R.id.tv_message)
46
+
47
+ setCanceledOnTouchOutside(true)
48
+ }
49
+
50
+ fun setMessage(message: String) {
51
+ mMessage = message
52
+ mTvMessage?.text = mMessage
53
+ }
54
+
55
+
56
+ override fun onClick(view: View?) {
57
+ when (view?.id) {
58
+ R.id.btn_ok -> {
59
+ mClickListener.onPositiveButtonClick()
60
+ }
61
+
62
+ R.id.btn_cancel -> {
63
+ mClickListener.onNegativeButtonClick()
64
+ }
65
+ }
66
+
67
+ this.dismiss()
68
+ }
69
+
70
+ interface OnConfirmButtonClickListener {
71
+ fun onPositiveButtonClick()
72
+ fun onNegativeButtonClick()
73
+ }
74
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/fragments/ChatFragment.kt ADDED
@@ -0,0 +1,888 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.fragments
2
+
3
+ import android.os.Bundle
4
+ import androidx.fragment.app.Fragment
5
+ import android.view.LayoutInflater
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.annotation.SuppressLint
9
+ import android.app.ActionBar.LayoutParams
10
+ import android.content.*
11
+ import android.graphics.Bitmap
12
+ import android.graphics.BitmapFactory
13
+ import android.net.Uri
14
+ import android.os.Build
15
+ import android.os.StrictMode
16
+ import android.os.StrictMode.ThreadPolicy
17
+ import android.provider.MediaStore
18
+ import android.telephony.SmsManager
19
+ import android.util.Log
20
+ import android.view.KeyEvent
21
+ import android.view.View.OnClickListener
22
+ import android.view.animation.AccelerateDecelerateInterpolator
23
+ import android.view.animation.AlphaAnimation
24
+ import android.view.animation.Animation
25
+ import android.view.animation.LinearInterpolator
26
+ import android.view.animation.RotateAnimation
27
+ import android.view.animation.TranslateAnimation
28
+ import android.widget.*
29
+ import androidx.annotation.RequiresApi
30
+ import androidx.recyclerview.widget.LinearLayoutManager
31
+ import androidx.recyclerview.widget.RecyclerView
32
+ import com.google.firebase.storage.FirebaseStorage
33
+ import com.matthaigh27.chatgptwrapper.MyApplication
34
+ import com.matthaigh27.chatgptwrapper.R
35
+ import com.matthaigh27.chatgptwrapper.adapters.ChatAdapter
36
+ import com.matthaigh27.chatgptwrapper.database.MyDatabase
37
+ import com.matthaigh27.chatgptwrapper.database.entity.ImageEntity
38
+ import com.matthaigh27.chatgptwrapper.widgets.ImagePickerWidget
39
+ import com.matthaigh27.chatgptwrapper.widgets.ImagePickerWidget.OnPositiveButtonClickListener
40
+ import com.matthaigh27.chatgptwrapper.models.*
41
+ import com.matthaigh27.chatgptwrapper.models.common.HelpCommandModel
42
+ import com.matthaigh27.chatgptwrapper.models.common.HelpPromptModel
43
+ import com.matthaigh27.chatgptwrapper.models.viewmodels.ChatMessageModel
44
+ import com.matthaigh27.chatgptwrapper.services.api.HttpClient
45
+ import com.matthaigh27.chatgptwrapper.services.api.HttpRisingInterface
46
+ import com.matthaigh27.chatgptwrapper.utils.Constants.*
47
+ import com.matthaigh27.chatgptwrapper.utils.Utils
48
+ import com.matthaigh27.chatgptwrapper.widgets.ContactDetailItem
49
+ import com.qw.photo.CoCo
50
+ import com.qw.photo.callback.CoCoAdapter
51
+ import com.qw.photo.callback.CoCoCallBack
52
+ import com.qw.photo.constant.Range
53
+ import com.qw.photo.pojo.PickResult
54
+ import com.qw.photo.pojo.TakeResult
55
+ import kotlinx.coroutines.CoroutineScope
56
+ import kotlinx.coroutines.Dispatchers
57
+ import kotlinx.coroutines.launch
58
+ import okhttp3.*
59
+ import org.json.JSONException
60
+ import org.json.JSONObject
61
+ import java.io.*
62
+ import java.util.*
63
+ import kotlin.collections.ArrayList
64
+
65
+ class ChatFragment : Fragment(), OnClickListener, HttpRisingInterface {
66
+ private lateinit var rootView: View
67
+ private var mContext: Context? = null
68
+
69
+ /** ui components for chatlist recyclerview */
70
+ private lateinit var mRvChatList: RecyclerView
71
+ private lateinit var mEtMessage: EditText
72
+
73
+ /** ui components for loading spinner */
74
+ private lateinit var mSpLoading: ImageView
75
+ private lateinit var mTvLoading: TextView
76
+ private lateinit var mLlLoading: LinearLayout
77
+
78
+ /** ui components for loading photo */
79
+ private lateinit var mLlLoadPhoto: LinearLayout
80
+ private lateinit var mIvLoadedPhoto: ImageView
81
+ private lateinit var mBtnCancelLoadPhoto: TextView
82
+
83
+ /** adapter for chat recyclerview and arraylist for store chatting history */
84
+ private var mMessageList: ArrayList<ChatMessageModel> = ArrayList()
85
+ private lateinit var mAdapter: ChatAdapter
86
+
87
+ /** when a user selects image by camera or gallery,
88
+ * these two variables are used to save image source and name */
89
+ private var mSelectedImage: ByteArray? = null
90
+ private var mSelectedImageName: String = ""
91
+
92
+ /**
93
+ * mImagePickerType is
94
+ * 'image_uplaod' when user is going to upload image
95
+ * 'image_picker' when user is going to pick image for prompting
96
+ */
97
+ private lateinit var mImagePickerWidget: ImagePickerWidget
98
+ private var mImagePickerType: String = ""
99
+
100
+ /** HttpClient for restful apis */
101
+ private lateinit var httpClient: HttpClient
102
+
103
+ /** list of help prompt commands */
104
+ private var mHelpPromptList: ArrayList<HelpPromptModel>? = null
105
+
106
+ /** animation variable for loading spinner */
107
+ private val rotate = RotateAnimation(
108
+ 0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
109
+ )
110
+
111
+ /** room database handler for local database */
112
+ private lateinit var mRoomDataHandler: MyDatabase
113
+
114
+ /** status variable that checks if widget in chatting interface does exist */
115
+ private var mIsExistWidget: Boolean = false
116
+
117
+ /**
118
+ * this is invoked when users click the message icon to send sms on contact detail dialog
119
+ * that is shown when a user search contacts
120
+ */
121
+ private var mSMSOnClickListener: ContactDetailItem.OnSMSClickListener? = null
122
+
123
+ override fun onCreateView(
124
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
125
+ ): View {
126
+ rootView = inflater.inflate(R.layout.fragment_chat, container, false)
127
+ init()
128
+ return rootView
129
+ }
130
+
131
+ private fun init() {
132
+ initEnvironment()
133
+ initValues()
134
+ initView()
135
+ initDatabase()
136
+
137
+ trainImages()
138
+ getAllPromptCommands()
139
+ }
140
+
141
+
142
+ private fun getAllPromptCommands() {
143
+ showLoading(true, "Loading Help Prompt Data")
144
+ httpClient.getALlHelpPromptCommands()
145
+ }
146
+
147
+
148
+ private fun initEnvironment() {
149
+ val policy = ThreadPolicy.Builder().permitAll().build()
150
+ StrictMode.setThreadPolicy(policy)
151
+ }
152
+
153
+ private fun initValues() {
154
+ mContext = context
155
+
156
+ httpClient = HttpClient(this)
157
+
158
+ rotate.duration = 3000
159
+ rotate.repeatCount = Animation.INFINITE
160
+ rotate.interpolator = LinearInterpolator()
161
+
162
+ initSMSOnClickListener()
163
+ }
164
+
165
+ private fun initView() {
166
+ this.mAdapter = ChatAdapter(mMessageList, mContext!!)
167
+ this.mAdapter.mOnSMSClickListener = mSMSOnClickListener
168
+ mRvChatList = rootView.findViewById<View>(R.id.chatRecycleView) as RecyclerView
169
+ mRvChatList.adapter = mAdapter
170
+
171
+ val sendSMSListener = object : ChatAdapter.MessageWidgetListener {
172
+ override fun sentSMS(phonenumber: String, message: String) {
173
+ mIsExistWidget = false
174
+ sendSms(phonenumber, message)
175
+ addMessage(
176
+ "You sent SMS with belowing content.\n\n " + "To: $phonenumber\n " + "Message: $message",
177
+ false
178
+ )
179
+ }
180
+
181
+ override fun canceledSMS() {
182
+ mIsExistWidget = false
183
+ addMessage("You canceled SMS.", false)
184
+ }
185
+
186
+ override fun sentHelpPrompt(prompt: String) {
187
+ mIsExistWidget = false
188
+ addMessage(prompt, true)
189
+ }
190
+
191
+ override fun canceledHelpPrompt() {
192
+ mIsExistWidget = false
193
+ addMessage("You canceled help command.", false)
194
+ }
195
+ }
196
+
197
+ mAdapter.setMessageWidgetListener(sendSMSListener)
198
+
199
+ val linearLayoutManager = LinearLayoutManager(mContext)
200
+ mRvChatList.layoutManager = linearLayoutManager
201
+
202
+ this.mEtMessage = rootView.findViewById(R.id.et_message)
203
+ mEtMessage.setOnKeyListener { _, keyCode, keyEvent ->
204
+ if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
205
+ addMessage(mEtMessage.text.toString(), true)
206
+ }
207
+ return@setOnKeyListener false
208
+ }
209
+
210
+ rootView.findViewById<View>(R.id.btn_send_message).setOnClickListener(this)
211
+ rootView.findViewById<View>(R.id.btn_image_upload).setOnClickListener(this)
212
+ rootView.findViewById<View>(R.id.btn_image_picker).setOnClickListener(this)
213
+
214
+ mSpLoading = rootView.findViewById(R.id.sp_loading)
215
+ mTvLoading = rootView.findViewById(R.id.tv_loading)
216
+ mLlLoading = rootView.findViewById(R.id.ll_loading)
217
+
218
+ mLlLoadPhoto = rootView.findViewById(R.id.ll_load_photo)
219
+ mIvLoadedPhoto = rootView.findViewById(R.id.iv_load_photo)
220
+ mBtnCancelLoadPhoto = rootView.findViewById(R.id.btn_cancel_load_photo)
221
+ mBtnCancelLoadPhoto.setOnClickListener(this)
222
+
223
+ initImagePickerWidget()
224
+ }
225
+
226
+ private fun initDatabase() {
227
+ mRoomDataHandler = MyDatabase.getDatabase(mContext!!)
228
+ }
229
+
230
+ private fun initSMSOnClickListener() {
231
+ mSMSOnClickListener = object : ContactDetailItem.OnSMSClickListener {
232
+ override fun onSMSClickListener(phoneNumber: String) {
233
+ addMessage(
234
+ "SMS",
235
+ isMe = false,
236
+ isSend = false,
237
+ isWidget = true,
238
+ widgetType = MSG_WIDGET_TYPE_SMS,
239
+ widgetDescription = phoneNumber
240
+ )
241
+ }
242
+
243
+ override fun onVoiceCallListener(phoneNumber: String, toName: String) {
244
+ addMessage(
245
+ message = "You made a voice call to $toName($phoneNumber)",
246
+ isMe = false,
247
+ isSend = false
248
+ )
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * set loading spinner visible
255
+ */
256
+ private fun setDisableActivity(enable: Boolean) {
257
+ runOnUIThread {
258
+ mEtMessage.isEnabled = enable
259
+ rootView.findViewById<View>(R.id.btn_send_message).isEnabled = enable
260
+ rootView.findViewById<View>(R.id.btn_image_upload).isEnabled = enable
261
+ rootView.findViewById<View>(R.id.btn_image_picker).isEnabled = enable
262
+ }
263
+ }
264
+
265
+ /**
266
+ * set loading spinner visible
267
+ */
268
+ private fun showLoading(visible: Boolean, text: String = "") {
269
+ runOnUIThread {
270
+ setDisableActivity(!visible)
271
+
272
+ if (visible) {
273
+ mSpLoading.startAnimation(rotate)
274
+ mLlLoading.visibility = View.VISIBLE
275
+ mTvLoading.text = text
276
+ } else {
277
+ mSpLoading.clearAnimation()
278
+ mLlLoading.visibility = View.GONE
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * show overlay when you picked image and users can cancel to load photo if users want
285
+ */
286
+ private fun showLoadPhotoOverlay(imageByteArray: ByteArray) {
287
+ mLlLoadPhoto.visibility = View.VISIBLE
288
+
289
+ mIvLoadedPhoto.setImageBitmap(
290
+ BitmapFactory.decodeByteArray(
291
+ /* data = */ imageByteArray, /* offset = */ 0, /* length = */ imageByteArray.size
292
+ )
293
+ )
294
+ }
295
+
296
+ /**
297
+ * Bind broadcast sent by MessageService
298
+ */
299
+ private val receiver = object : BroadcastReceiver() {
300
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
301
+ override fun onReceive(context: Context, intent: Intent) {
302
+ Log.d(TAG, "Receive")
303
+ }
304
+ }
305
+
306
+
307
+ /**
308
+ * get download url from firebase imagename in response and generate download url from the imagename
309
+ * With getBitmapFromURL method, get bitmap of the image and set it to mSelectedImage to display it to chatlist
310
+ * Finally, run addmessage and insert it to chat recyclerview
311
+ *
312
+ * @param imageName the firebase store imagename
313
+ */
314
+ private fun getImageResponse(imageName: String, imageDesc: String) {
315
+ if (imageName.isEmpty() && imageDesc.isNotEmpty()) {
316
+ addMessage(message = imageDesc, isMe = false)
317
+ return
318
+ }
319
+
320
+ showLoading(visible = true, text = LOADING_DOWNLOADING_IMAGE)
321
+
322
+ val imageNameToUpload = "images/$imageName"
323
+
324
+ val storageReference = FirebaseStorage.getInstance().getReference(imageNameToUpload)
325
+ storageReference.downloadUrl.addOnSuccessListener { uri ->
326
+ try {
327
+ val image = Utils.instance.getBitmapFromURL(uri.toString())
328
+ if (image == null) showToast("can not get bitmap from url")
329
+ val byteArrayOutputStream = ByteArrayOutputStream()
330
+
331
+ val isSuccess =
332
+ image!!.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
333
+ if (!isSuccess) {
334
+ showToast("Fail to compress image")
335
+ }
336
+ mSelectedImage = byteArrayOutputStream.toByteArray()
337
+ } catch (e: Exception) {
338
+ showToast("cannot get download url from firebase store")
339
+ e.printStackTrace()
340
+ }
341
+
342
+ addMessage(message = imageDesc, isMe = false)
343
+ showLoading(visible = false)
344
+ }
345
+ return
346
+ }
347
+
348
+ override fun onResume() {
349
+ super.onResume()
350
+ updateChangedUserData()
351
+ }
352
+
353
+ private fun updateChangedUserData() {
354
+ trainContacts()
355
+ }
356
+
357
+ /**
358
+ * Open Help Command Widget with analysing string command
359
+ */
360
+ private fun openHelpCommandWidget(strCommand: String) {
361
+ try {
362
+ val command: HelpCommandModel = Utils.instance.getHelpCommandFromStr(strCommand)
363
+ if (command.isMainCommand()) {
364
+ when (command.mainCommandName) {
365
+ "sms" -> {
366
+ addMessage(
367
+ message = "SMS",
368
+ isMe = false,
369
+ isSend = false,
370
+ isWidget = true,
371
+ widgetType = MSG_WIDGET_TYPE_SMS
372
+ )
373
+ }
374
+
375
+ else -> {
376
+ mHelpPromptList!!.forEach { model ->
377
+ if (model.name == command.mainCommandName) {
378
+ addMessage(
379
+ "Help Prompt Command",
380
+ isMe = true,
381
+ isSend = false,
382
+ isWidget = true,
383
+ widgetType = MSG_WIDGET_TYPE_HELP_PRMOPT,
384
+ widgetDescription = model.toString()
385
+ )
386
+ return
387
+ }
388
+ }
389
+ addMessage(
390
+ message = "No such command name exists.", isMe = false, isSend = false
391
+ )
392
+ }
393
+ }
394
+ } else {
395
+ if (command.assistCommandName == "all") {
396
+ val usage =
397
+ "usage:\n" + "- help command: /help [command name]\n" + "- prompt command: /<command name>\n\n"
398
+ var strHelpList = "help prompt commands:"
399
+ mHelpPromptList!!.forEach { model ->
400
+ strHelpList += "\n- " + model.name
401
+ }
402
+
403
+ addMessage(message = usage + strHelpList, isMe = false, isSend = false)
404
+ } else {
405
+ var strHelpDesc = ""
406
+ mHelpPromptList!!.forEach { model ->
407
+ if (model.name == command.assistCommandName) {
408
+ var strTags = ""
409
+ model.tags!!.forEach { tag ->
410
+ strTags += " $tag"
411
+ }
412
+ strHelpDesc = "description: " + model.description + "\ntags:" + strTags
413
+ }
414
+ }
415
+ if (strHelpDesc.isEmpty()) addMessage(
416
+ message = "No such command name exists.", isMe = false, isSend = false
417
+ )
418
+ else addMessage(message = strHelpDesc, isMe = false, isSend = false)
419
+ }
420
+ }
421
+ } catch (e: Exception) {
422
+ e.printStackTrace()
423
+ showToast(e.message.toString())
424
+ }
425
+ }
426
+
427
+ /**
428
+ * add message to chat list
429
+ * if users picked image from camera or gallery, send post request to `sendNotification` to ask generally
430
+ * else do 'imageRelateness' to search image
431
+ *
432
+ * @param message message content for chat list
433
+ * @param isMe this identify if this is sent by langchain server or users send message to server
434
+ * @param isSend is boolean that checks if you send request to server
435
+ * @param isWidget is boolean that checks if message item has widget
436
+ * @param widgetType is type of Widget ex: SMS, HELP_COMMAND, etc
437
+ * @param widgetDescription is string that saves information for widget
438
+ */
439
+ @SuppressLint("NotifyDataSetChanged")
440
+ private fun addMessage(
441
+ message: String,
442
+ isMe: Boolean,
443
+ isSend: Boolean = true,
444
+ isWidget: Boolean = false,
445
+ widgetType: String = "",
446
+ widgetDescription: String = ""
447
+ ) {
448
+ if ((message.isEmpty() && mSelectedImage == null) || mIsExistWidget) return
449
+ if (isWidget) {
450
+ if (widgetType != MSG_WIDGET_TYPE_SEARCH_CONTACT) {
451
+ mIsExistWidget = true
452
+ }
453
+ }
454
+
455
+ val msg = ChatMessageModel()
456
+ msg.message = message
457
+ msg.isMe = isMe
458
+ msg.isWidget = isWidget
459
+ msg.widgetType = widgetType
460
+ msg.widgetDescription = widgetDescription
461
+
462
+ /**
463
+ * if users picked some image from camera or gallery, add the image to chatting message
464
+ */
465
+ if (mSelectedImage != null) {
466
+ msg.image = mSelectedImage
467
+ mSelectedImage = null
468
+ }
469
+
470
+ /**
471
+ * if users picked some image from camera or gallery, the image upload to firebase store
472
+ * mSelectedImageName is uuid created uploading to firebase store
473
+ */
474
+ if (mSelectedImageName.isNotEmpty()) {
475
+ msg.imageName = mSelectedImageName
476
+ mSelectedImageName = ""
477
+ }
478
+
479
+ runOnUIThread {
480
+ mLlLoadPhoto.visibility = View.GONE
481
+
482
+ mMessageList.add(msg)
483
+ mAdapter.notifyDataSetChanged()
484
+ mEtMessage.setText("")
485
+ mRvChatList.scrollTo(/* x = */ 1000, /* y = */ -1000)
486
+ mRvChatList.scrollToPosition(mMessageList.size - 1)
487
+ }
488
+
489
+ if ((message.isNotEmpty() && message.first() == '/') && isMe) {
490
+ openHelpCommandWidget(message)
491
+ return
492
+ }
493
+
494
+ if (isMe) {
495
+ if (!isSend) {
496
+ return
497
+ }
498
+
499
+ showLoading(visible = true, text = LOADING_ASKING_TO_GPT)
500
+ if (msg.image != null) {
501
+ httpClient.callImageRelatedness(msg.imageName)
502
+ } else {
503
+ httpClient.callSendNotification(message)
504
+ }
505
+ }
506
+ }
507
+
508
+ override fun onClick(view: View) {
509
+ when (view.id) {
510
+ R.id.btn_send_message -> {
511
+ addMessage(mEtMessage.text.toString(), true)
512
+ }
513
+
514
+ R.id.btn_image_upload -> {
515
+ mImagePickerType = PICKERTYPE_IMAGE_UPLOAD
516
+ if(rootView.findViewById<View>(R.id.ll_toolbar).visibility == View.VISIBLE)
517
+ hideSlidingWidget()
518
+ else
519
+ showSlidingWidget()
520
+ }
521
+
522
+ R.id.btn_image_picker -> {
523
+ mImagePickerType = PICKERTYPE_IMAGE_PICK
524
+ if(rootView.findViewById<View>(R.id.ll_toolbar).visibility == View.VISIBLE)
525
+ hideSlidingWidget()
526
+ else
527
+ showSlidingWidget()
528
+ }
529
+
530
+ R.id.btn_cancel_load_photo -> {
531
+ mLlLoadPhoto.visibility = View.GONE
532
+ mSelectedImageName = ""
533
+ mSelectedImage = null
534
+ }
535
+ }
536
+ }
537
+
538
+ /**
539
+ * open browser with url
540
+ * @param url to open with browser
541
+ */
542
+ private fun openBrowser(url: String) {
543
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
544
+ startActivity(Intent.createChooser(intent, "Browse with"))
545
+ }
546
+
547
+ /**
548
+ * calls when finish picking image
549
+ *
550
+ * @param imageByteData is bytearray of picked image
551
+ * @param type if users are going to upload image or pick image for query
552
+ */
553
+ private fun pickedImage(imageByteData: ByteArray, type: String) {
554
+ if (type == "image_upload") {
555
+ uploadImageToFirebaseStorage(imageByteData)
556
+ } else {
557
+ uploadSearchImage(imageByteData)
558
+ }
559
+ }
560
+
561
+ /**
562
+ * toast message is invoked when error happens
563
+ */
564
+ private fun showToast(message: String) {
565
+ runOnUIThread {
566
+ Toast.makeText(
567
+ mContext, message, Toast.LENGTH_SHORT
568
+ ).show()
569
+ }
570
+ }
571
+
572
+
573
+ /**
574
+ * this can show dialog with camera and gallery icon.
575
+ * when you click camera icon, camera application runs and users can get an image by capturing using camera.
576
+ * when you click gallery icon, users can select image in your storage.
577
+ * A picked image converts into bytearray data and upload to firebase storage.
578
+ */
579
+ private fun initImagePickerWidget() {
580
+ mImagePickerWidget = ImagePickerWidget(mContext!!)
581
+
582
+ val myImplementation = object : OnPositiveButtonClickListener {
583
+ override fun onPositiveBtnClick(isCamera: Boolean?) {
584
+ if (isCamera == true) {
585
+ CoCo.with(activity!!).take(Utils.instance.createSDCardFile())
586
+ .start(object : CoCoAdapter<TakeResult>() {
587
+ override fun onSuccess(data: TakeResult) {
588
+ val byteArray: ByteArray =
589
+ Utils.instance.getBytesFromPath(data.savedFile!!.absolutePath)
590
+ pickedImage(
591
+ byteArray, mImagePickerType
592
+ )
593
+ }
594
+
595
+ override fun onFailed(exception: Exception) {
596
+ super.onFailed(exception)
597
+ showToast("Fail to pick image. Please try again.")
598
+ }
599
+ })
600
+ } else {
601
+ CoCo.with(activity!!).pick().range(Range.PICK_CONTENT)
602
+ .start(object : CoCoCallBack<PickResult> {
603
+
604
+ override fun onSuccess(data: PickResult) {
605
+ val byteArray: ByteArray? =
606
+ Utils.instance.convertImageToByte(data.originUri)
607
+ if (byteArray == null) showToast("can not find such a file")
608
+ pickedImage(byteArray!!, mImagePickerType)
609
+ }
610
+
611
+ override fun onFailed(exception: Exception) {
612
+ showToast("Fail to pick image. Please try again.")
613
+ }
614
+ })
615
+ }
616
+ }
617
+ }
618
+
619
+ mImagePickerWidget.setOnClickListener(myImplementation)
620
+
621
+ val slidingWidget = rootView.findViewById<LinearLayout>(R.id.ll_toolbar)
622
+ slidingWidget.addView(mImagePickerWidget)
623
+ }
624
+
625
+ private fun uploadSearchImage(imageByteArray: ByteArray) {
626
+ showLoading(true, LOADING_ANALYZING_IMAGE)
627
+ val storageRef = FirebaseStorage.getInstance().reference
628
+ val uuid = UUID.randomUUID()
629
+ val imageName = "images/${uuid}"
630
+ val imageRef = storageRef.child(imageName)
631
+
632
+ val uploadTask = imageRef.putBytes(imageByteArray)
633
+ uploadTask.addOnFailureListener {
634
+ showLoading(false)
635
+ }.addOnSuccessListener {
636
+ Log.d(TAG, "Success upload to firebase storage")
637
+ showLoading(false)
638
+
639
+ mSelectedImageName = "$uuid"
640
+ mSelectedImage = imageByteArray
641
+
642
+ showLoadPhotoOverlay(imageByteArray)
643
+ }
644
+ }
645
+
646
+ /**
647
+ * @param imageByteArray ByteArray data for image to upload to firebase storage
648
+ */
649
+ private fun uploadImageToFirebaseStorage(imageByteArray: ByteArray) {
650
+ showLoading(true, LOADING_UPLOADING_IAMGE)
651
+ val storageRef = FirebaseStorage.getInstance().reference
652
+ val uuid = UUID.randomUUID()
653
+ val imageName = "images/${uuid}"
654
+ val imageRef = storageRef.child(imageName)
655
+
656
+ val uploadTask = imageRef.putBytes(imageByteArray)
657
+ uploadTask.addOnFailureListener {
658
+ showLoading(false)
659
+ }.addOnSuccessListener {
660
+ Log.d(TAG, "Success upload to firebase storage")
661
+
662
+ showLoading(false)
663
+ httpClient.callImageUpload("$uuid")
664
+ }
665
+ }
666
+
667
+ override fun onSuccessResult(msg: String) {
668
+ showLoading(false)
669
+ try {
670
+ val json = JSONObject(msg)
671
+ if (json.has(RESPONSE_TYPE_PROGRAM)) {
672
+ when (json.getString(RESPONSE_TYPE_PROGRAM)) {
673
+ RESPONSE_TYPE_BROWSER -> {
674
+ addMessage(json.getString(RESPONSE_TYPE_URL), false)
675
+ openBrowser(json.getString(RESPONSE_TYPE_URL))
676
+ return
677
+ }
678
+
679
+ RESPONSE_TYPE_ALERT -> {
680
+ MyApplication.appContext.showNotification(
681
+ json.getString(
682
+ RESPONSE_TYPE_CONTENT
683
+ )
684
+ )
685
+ return
686
+ }
687
+
688
+ RESPONSE_TYPE_MESSAGE -> {
689
+ addMessage(json.getString(RESPONSE_TYPE_CONTENT), false)
690
+ return
691
+ }
692
+
693
+ RESPONSE_TYPE_IMAGE -> {
694
+ try {
695
+ val imageRes = JSONObject(json.getString(RESPONSE_TYPE_CONTENT))
696
+
697
+ val imageName = if (imageRes.has("image_name")) {
698
+ imageRes["image_name"] as String
699
+ } else {
700
+ ""
701
+ }
702
+
703
+ val imageDesc = if (imageRes.has("image_desc")) {
704
+ imageRes["image_desc"] as String
705
+ } else {
706
+ ""
707
+ }
708
+
709
+ getImageResponse(imageName, imageDesc)
710
+ } catch (e: Exception) {
711
+ e.printStackTrace()
712
+ }
713
+ return
714
+ }
715
+
716
+ RESPONSE_TYPE_SMS -> {
717
+ addMessage(
718
+ json.getString(RESPONSE_TYPE_CONTENT),
719
+ false,
720
+ isSend = false,
721
+ isWidget = true,
722
+ widgetType = MSG_WIDGET_TYPE_SMS
723
+ )
724
+ }
725
+
726
+ RESPONSE_TYPE_HELP_COMMAND -> {
727
+ try {
728
+ mHelpPromptList = Utils.instance.getHelpCommandListFromJsonString(
729
+ json.getString(RESPONSE_TYPE_CONTENT)
730
+ )
731
+ } catch (e: java.lang.Exception) {
732
+ e.printStackTrace()
733
+ showToast("JSON Error occured")
734
+ }
735
+ }
736
+
737
+ RESPONSE_TYPE_CONTACT -> {
738
+ try {
739
+ addMessage(
740
+ message = "Contacts that you are looking for.",
741
+ isMe = false,
742
+ isSend = false,
743
+ isWidget = true,
744
+ widgetType = MSG_WIDGET_TYPE_SEARCH_CONTACT,
745
+ widgetDescription = json.getString(RESPONSE_TYPE_CONTENT)
746
+ )
747
+ } catch (e: Exception) {
748
+ e.printStackTrace()
749
+ }
750
+ }
751
+ }
752
+ }
753
+ } catch (e: JSONException) {
754
+ e.printStackTrace()
755
+ addMessage(msg, false)
756
+ }
757
+ }
758
+
759
+ override fun onFailureResult(msg: String) {
760
+ showLoading(false)
761
+
762
+ showToast(msg)
763
+ }
764
+
765
+ private fun queryImagesFromExternalStorage(contentResolver: ContentResolver): ArrayList<Uri> {
766
+ val listOfImageUris = ArrayList<Uri>()
767
+
768
+ val projection = arrayOf(MediaStore.Images.Media._ID)
769
+
770
+ val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"
771
+
772
+ contentResolver.query(
773
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, sortOrder
774
+ )?.use { cursor ->
775
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
776
+ while (cursor.moveToNext()) {
777
+ val id = cursor.getLong(idColumn)
778
+ val contentUri = Uri.withAppendedPath(
779
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString()
780
+ )
781
+ listOfImageUris.add(contentUri)
782
+ }
783
+ }
784
+ return listOfImageUris
785
+ }
786
+
787
+ private fun trainImages() {
788
+ CoroutineScope(Dispatchers.IO).launch {
789
+ val images = queryImagesFromExternalStorage(requireContext().contentResolver)
790
+ val originalImages = mRoomDataHandler.imageDao().getAllImages()
791
+
792
+ images.forEach { uri ->
793
+ var isExist = false
794
+ val path = Utils.instance.getRealPathFromUri(requireContext(), uri)
795
+ for (i in originalImages.indices) {
796
+ val entity: ImageEntity = originalImages[i]
797
+ if (entity.path == path) {
798
+ isExist = true
799
+ break
800
+ }
801
+ }
802
+ if (!isExist) {
803
+ val byteArray = Utils.instance.getBytesFromPath(path)
804
+ val uuid = uploadImageToFirebaseStorage(byteArray)
805
+
806
+ if (path != null)
807
+ mRoomDataHandler.imageDao().insertImage(ImageEntity(0, path, "$uuid"))
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ private fun trainContacts() {
814
+ showLoading(true, "Train Contacts")
815
+ val contacts = Utils.instance.getContacts(mContext!!)
816
+ CoroutineScope(Dispatchers.Main).launch {
817
+ val changedContacts = Utils.instance.getChangedContacts(contacts, mRoomDataHandler)
818
+ httpClient.trainContacts(changedContacts)
819
+ }
820
+ }
821
+
822
+ fun sendSms(phoneNumber: String, message: String) {
823
+ try {
824
+ val smsManager = SmsManager.getDefault()
825
+ val parts = smsManager.divideMessage(message)
826
+ smsManager.sendMultipartTextMessage(/* destinationAddress = */ phoneNumber, /* scAddress = */
827
+ null, /* parts = */
828
+ parts, /* sentIntents = */
829
+ null, /* deliveryIntents = */
830
+ null)
831
+ } catch (e: SecurityException) {
832
+ e.printStackTrace()
833
+ } catch (e: Exception) {
834
+ e.printStackTrace()
835
+ }
836
+ }
837
+
838
+ @SuppressLint("UseRequireInsteadOfGet")
839
+ fun runOnUIThread(action: () -> Unit) {
840
+ requireActivity().runOnUiThread {
841
+ action()
842
+ }
843
+ }
844
+
845
+ private fun showSlidingWidget() {
846
+ val slidingWidget = rootView.findViewById<View>(R.id.ll_toolbar)
847
+
848
+ val dy = slidingWidget.measuredHeight.toFloat()
849
+ slidingWidget.visibility = View.VISIBLE
850
+
851
+ val anim = TranslateAnimation(0f, 0f, dy, 0f).apply {
852
+ duration = 150 // Set the animation duration, e.g., 300ms
853
+ interpolator = AccelerateDecelerateInterpolator()
854
+ setAnimationListener(object : Animation.AnimationListener {
855
+ override fun onAnimationStart(animation: Animation?) {
856
+ slidingWidget.visibility = View.VISIBLE
857
+ }
858
+
859
+ override fun onAnimationEnd(animation: Animation?) {}
860
+
861
+ override fun onAnimationRepeat(animation: Animation?) {}
862
+ })
863
+ }
864
+
865
+ slidingWidget.startAnimation(anim)
866
+ }
867
+
868
+ private fun hideSlidingWidget() {
869
+ val slidingWidget = rootView.findViewById<View>(R.id.ll_toolbar)
870
+
871
+ val anim = AlphaAnimation(1f, 0f).apply {
872
+ duration = 100 // Set the animation duration, e.g., 300ms
873
+ interpolator = AccelerateDecelerateInterpolator()
874
+ setAnimationListener(object : Animation.AnimationListener {
875
+ override fun onAnimationStart(animation: Animation?) {}
876
+
877
+ override fun onAnimationEnd(animation: Animation?) {
878
+ slidingWidget.visibility = View.GONE
879
+ }
880
+
881
+ override fun onAnimationRepeat(animation: Animation?) {}
882
+ })
883
+ }
884
+
885
+ slidingWidget.startAnimation(anim)
886
+ }
887
+ }
888
+
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/ContactModel.kt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.common
2
+
3
+ class ContactModel {
4
+ var id: String = ""
5
+ var name: String = ""
6
+ var phoneList: ArrayList<String>? = null
7
+ var status: String = ""
8
+
9
+ init {
10
+ phoneList = ArrayList()
11
+ }
12
+
13
+ fun setData(id: String, name: String, phoneList: ArrayList<String>, status: String) {
14
+ this.id = id
15
+ this.name = name
16
+ this.phoneList = phoneList
17
+ this.status = status
18
+ }
19
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/HelpCommandModel.kt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.common
2
+
3
+ class HelpCommandModel {
4
+ var mainCommandName: String = ""
5
+ var assistCommandName: String = ""
6
+
7
+ fun isMainCommand(): Boolean {
8
+ return mainCommandName != "help"
9
+ }
10
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/HelpPromptModel.kt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.common
2
+
3
+ import com.google.gson.Gson
4
+ import com.google.gson.GsonBuilder
5
+
6
+ class HelpPromptModel {
7
+ var name: String = ""
8
+ var description: String = ""
9
+ var prompt: String = ""
10
+ var tags: ArrayList<String>? = null
11
+
12
+ override fun toString(): String {
13
+ val gson = Gson()
14
+ val str = gson.toJson(this)
15
+ return str
16
+ }
17
+
18
+ companion object {
19
+ fun initModelWithString(strJson: String): HelpPromptModel {
20
+ val gson = Gson()
21
+ val model: HelpPromptModel = gson.fromJson(strJson, HelpPromptModel::class.java)
22
+ return model
23
+ }
24
+ }
25
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/common/ImagePromptModel.kt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.common
2
+
3
+ class ImagePromptModel {
4
+ var id: String = ""
5
+ var path: String = ""
6
+
7
+ constructor(id: String, path: String) {
8
+ this.id = id
9
+ this.path = path
10
+ }
11
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/requestmodels/RequestBodyModel.kt ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.requestmodels
2
+
3
+ import com.matthaigh27.chatgptwrapper.MyApplication
4
+ import org.json.JSONException
5
+ import org.json.JSONObject
6
+
7
+ class RequestBodyModel(builder: Builder) {
8
+
9
+ /** this identify request type
10
+ * example: it will be 'message' when users send message, 'image' when users upload image
11
+ */
12
+ var type: String = ""
13
+ var token: String = ""
14
+ var message: String = ""
15
+ var imageName: String = ""
16
+ var uuid: String = ""
17
+
18
+ init {
19
+ this.token = MyApplication.appContext.getFCMToken()
20
+ this.uuid = MyApplication.appContext.getUUID()
21
+ this.message = builder.message
22
+ this.imageName = builder.imageName
23
+ }
24
+
25
+ @Throws(JSONException::class)
26
+ fun buildJsonObject(): JSONObject {
27
+
28
+ val jsonObject = JSONObject()
29
+ jsonObject.accumulate("type", type)
30
+ jsonObject.accumulate("token", token)
31
+ jsonObject.accumulate("message", message)
32
+ jsonObject.accumulate("image_name", imageName)
33
+ jsonObject.accumulate("uuid", uuid)
34
+
35
+ return jsonObject
36
+ }
37
+
38
+ class Builder {
39
+ var type: String = ""
40
+ var token: String = ""
41
+ var message: String = ""
42
+ var imageName: String = ""
43
+ var uuid: String = ""
44
+
45
+ constructor() {
46
+
47
+ }
48
+
49
+ constructor(request: RequestBodyModel) {
50
+ this.type = request.type
51
+ this.token = request.token
52
+ this.message = request.message
53
+ this.imageName = request.imageName
54
+ this.uuid = request.uuid
55
+ }
56
+
57
+ fun type(type: String): Builder {
58
+ this.type = type
59
+ return this
60
+ }
61
+
62
+ fun token(token: String): Builder {
63
+ this.token = token
64
+ return this
65
+ }
66
+
67
+ fun message(message: String): Builder {
68
+ this.message = message
69
+ return this
70
+ }
71
+
72
+ fun imageName(imageName: String): Builder {
73
+ this.imageName = imageName
74
+ return this
75
+ }
76
+
77
+ fun uuid(uuid: String): Builder {
78
+ this.uuid = uuid
79
+ return this
80
+ }
81
+
82
+ fun build(): RequestBodyModel {
83
+ return RequestBodyModel(this)
84
+ }
85
+ }
86
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/requestmodels/RequestTrainContactModel.kt ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.requestmodels
2
+
3
+ import com.matthaigh27.chatgptwrapper.MyApplication
4
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
5
+ import com.matthaigh27.chatgptwrapper.utils.Utils
6
+ import org.json.JSONArray
7
+ import org.json.JSONException
8
+ import org.json.JSONObject
9
+
10
+ class RequestTrainContactModel(builder: Builder) {
11
+
12
+ /** this identify request type
13
+ * example: it will be 'message' when users send message, 'image' when users upload image
14
+ */
15
+ var type: String = ""
16
+ var token: String = ""
17
+ var contacts = JSONArray()
18
+ var uuid: String = ""
19
+
20
+ init {
21
+ this.token = MyApplication.appContext.getFCMToken()
22
+ this.uuid = MyApplication.appContext.getUUID()
23
+ this.contacts = builder.contacts
24
+ }
25
+
26
+ @Throws(JSONException::class)
27
+ fun buildJsonObject(): JSONObject {
28
+
29
+ val jsonObject = JSONObject()
30
+ jsonObject.accumulate("type", type)
31
+ jsonObject.accumulate("token", token)
32
+ jsonObject.accumulate("contacts", contacts)
33
+ jsonObject.accumulate("uuid", uuid)
34
+
35
+ return jsonObject
36
+ }
37
+
38
+ class Builder {
39
+ var type: String = ""
40
+ var token: String = ""
41
+ var contacts = JSONArray()
42
+ var uuid: String = ""
43
+
44
+ constructor() {
45
+
46
+ }
47
+
48
+ constructor(request: RequestTrainContactModel) {
49
+ this.type = request.type
50
+ this.token = request.token
51
+ this.uuid = request.uuid
52
+ this.contacts = request.contacts
53
+ }
54
+
55
+ fun type(type: String): Builder {
56
+ this.type = type
57
+ return this
58
+ }
59
+
60
+ fun token(token: String): Builder {
61
+ this.token = token
62
+ return this
63
+ }
64
+
65
+ fun contacts(contacts: ArrayList<ContactModel>): Builder {
66
+ this.contacts = Utils.instance.convertContactModelToJsonArray(contacts)
67
+ return this
68
+ }
69
+
70
+ fun uuid(uuid: String): Builder {
71
+ this.uuid = uuid
72
+ return this
73
+ }
74
+
75
+ fun build(): RequestTrainContactModel {
76
+ return RequestTrainContactModel(this)
77
+ }
78
+ }
79
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/models/viewmodels/ChatMessageModel.kt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.models.viewmodels
2
+
3
+ import androidx.lifecycle.ViewModel
4
+
5
+ /**
6
+ * ChatModel for Chat RecyclerView
7
+ */
8
+ class ChatMessageModel: ViewModel() {
9
+ var message: String = ""
10
+ var isMe: Boolean = true
11
+ var image: ByteArray? = null
12
+ var imageName: String = ""
13
+ var visibleFeedback = false
14
+ var feedback = 0
15
+ var isWidget: Boolean = false
16
+ var widgetType = ""
17
+ var widgetDescription = ""
18
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/MessageService.kt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.services
2
+
3
+ import android.content.Intent
4
+ import android.util.Log
5
+ import com.google.firebase.messaging.FirebaseMessagingService
6
+ import com.google.firebase.messaging.RemoteMessage
7
+ import com.matthaigh27.chatgptwrapper.utils.Constants.TAG
8
+
9
+ class MessageService : FirebaseMessagingService() {
10
+
11
+ /**
12
+ * this function is called when langchain server pushs notification into FCM
13
+ * @param remoteMessage is sent by FCM when langchain server pushs notification
14
+ */
15
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
16
+ super.onMessageReceived(remoteMessage)
17
+ if (remoteMessage.notification != null) {
18
+ Log.d(
19
+ TAG, "Message Notification Body: " + remoteMessage.notification!!.body
20
+ )
21
+
22
+ /** intent for sending broadcast to ChatActivity */
23
+ val intent = Intent()
24
+ intent.action = "android.intent.action.MAIN"
25
+ intent.putExtra("message", remoteMessage.notification!!.body)
26
+
27
+ /** send broadcast to ChatActivity
28
+ * So ChatActivity can receive remoteMessage from this service */
29
+ sendBroadcast(intent)
30
+ }
31
+ }
32
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/api/HttpClient.kt ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.services.api
2
+
3
+ import android.util.Log
4
+ import com.matthaigh27.chatgptwrapper.BuildConfig
5
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
6
+ import com.matthaigh27.chatgptwrapper.models.requestmodels.RequestBodyModel
7
+ import com.matthaigh27.chatgptwrapper.models.requestmodels.RequestTrainContactModel
8
+ import com.matthaigh27.chatgptwrapper.utils.Constants
9
+ import com.matthaigh27.chatgptwrapper.utils.Constants.GET
10
+ import com.matthaigh27.chatgptwrapper.utils.Constants.POST
11
+ import com.matthaigh27.chatgptwrapper.utils.ReqType
12
+ import okhttp3.Call
13
+ import okhttp3.Callback
14
+ import okhttp3.OkHttpClient
15
+ import okhttp3.Request
16
+ import okhttp3.RequestBody
17
+ import okhttp3.Response
18
+ import org.json.JSONException
19
+ import org.json.JSONObject
20
+ import java.io.IOException
21
+ import java.util.concurrent.TimeUnit
22
+
23
+ class HttpClient {
24
+ /* Server URL and Api Endpoints */
25
+ val SERVER_URL = BuildConfig.BASE_URL
26
+ val SEND_NOTIFICATION_URL = SERVER_URL + "sendNotification"
27
+ val IMAGE_RELATEDNESS = SERVER_URL + "image_relatedness"
28
+ val UPLOAD_IMAGE = SERVER_URL + "uploadImage"
29
+ val GET_ALL_HELP_COMMANDS = SERVER_URL + "commands"
30
+ val TRAIN_CONTACTS = SERVER_URL + "train/contacts"
31
+
32
+ var mCallback: HttpRisingInterface
33
+
34
+ constructor(callback: HttpRisingInterface) {
35
+ mCallback = callback
36
+ }
37
+
38
+ private fun sendOkHttpRequest(postBody: String, postUrl: String, method: String) {
39
+ val body: RequestBody = RequestBody.create(Constants.JSON, postBody)
40
+
41
+ /**
42
+ * set okhttpclient timeout to 120s
43
+ */
44
+ var request: Request? = null
45
+ if (method == POST) request = Request.Builder().url(postUrl).post(body).build()
46
+ else request = Request.Builder().url(postUrl).get().build()
47
+
48
+ val client =
49
+ OkHttpClient.Builder().connectTimeout(Constants.CUSTOM_TIMEOUT, TimeUnit.SECONDS)
50
+ .readTimeout(Constants.CUSTOM_TIMEOUT, TimeUnit.SECONDS)
51
+ .writeTimeout(Constants.CUSTOM_TIMEOUT, TimeUnit.SECONDS).build()
52
+
53
+ client.newCall(request).enqueue(object : Callback {
54
+ override fun onFailure(call: Call, e: IOException) {
55
+ /**
56
+ * Handle failure
57
+ */
58
+ e.printStackTrace()
59
+
60
+ mCallback.onFailureResult("Fail to send request to server. Please ask again.")
61
+ }
62
+
63
+ override fun onResponse(call: Call, response: Response) {
64
+ val myResponse = response.body()!!.string()
65
+ Log.d(Constants.TAG, myResponse)
66
+
67
+ try {
68
+ val json = JSONObject(myResponse)["result"].toString()
69
+ mCallback.onSuccessResult(json)
70
+ } catch (e: JSONException) {
71
+ mCallback.onFailureResult(myResponse)
72
+ e.printStackTrace()
73
+ }
74
+ }
75
+ })
76
+ }
77
+
78
+ /* call sendNotification */
79
+ fun callSendNotification(message: String) {
80
+ sendOkHttpRequest(
81
+ RequestBodyModel.Builder().message(message).type(ReqType.instance.MESSAGE).build()
82
+ .buildJsonObject().toString(), SEND_NOTIFICATION_URL, POST
83
+ )
84
+ }
85
+
86
+ /* call image_relatedness */
87
+ fun callImageRelatedness(imageName: String) {
88
+ sendOkHttpRequest(
89
+ RequestBodyModel.Builder().imageName(imageName).type(ReqType.instance.MESSAGE).build()
90
+ .buildJsonObject().toString(), IMAGE_RELATEDNESS, POST
91
+ )
92
+ }
93
+
94
+ /* call image_upload */
95
+ fun callImageUpload(imageName: String) {
96
+ sendOkHttpRequest(
97
+ RequestBodyModel.Builder().imageName(imageName).type(ReqType.instance.IMAGE_UPLOAD)
98
+ .build().buildJsonObject().toString(), UPLOAD_IMAGE, POST
99
+ )
100
+ }
101
+
102
+ fun getALlHelpPromptCommands() {
103
+ sendOkHttpRequest(
104
+ RequestBodyModel.Builder().build().buildJsonObject().toString(),
105
+ GET_ALL_HELP_COMMANDS,
106
+ GET
107
+ )
108
+ }
109
+
110
+ fun trainContacts(contacts: ArrayList<ContactModel>) {
111
+ sendOkHttpRequest(
112
+ RequestTrainContactModel.Builder().contacts(contacts).build().buildJsonObject().toString(),
113
+ TRAIN_CONTACTS,
114
+ POST
115
+ )
116
+ }
117
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/services/api/HttpRisingInterface.kt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.services.api;
2
+
3
+ interface HttpRisingInterface {
4
+ fun onSuccessResult(msg: String)
5
+
6
+ fun onFailureResult(msg: String)
7
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/Constants.java ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.utils;
2
+
3
+ import okhttp3.MediaType;
4
+
5
+ /**
6
+ * const variables
7
+ */
8
+ public class Constants {
9
+ /**
10
+ * app names
11
+ */
12
+ public static String RESPONSE_TYPE_BROWSER = "browser";
13
+ public static String RESPONSE_TYPE_ALERT = "alert";
14
+ public static String RESPONSE_TYPE_MESSAGE = "message";
15
+ public static String RESPONSE_TYPE_PROGRAM = "program";
16
+ public static String RESPONSE_TYPE_CONTENT = "content";
17
+ public static String RESPONSE_TYPE_URL = "url";
18
+ public static String RESPONSE_TYPE_IMAGE = "image";
19
+ public static String RESPONSE_TYPE_HELP_COMMAND = "help_command";
20
+ public static String RESPONSE_TYPE_SMS = "sms";
21
+ public static String RESPONSE_TYPE_CONTACT = "contact";
22
+
23
+ /**
24
+ * message widget type
25
+ */
26
+ public static String MSG_WIDGET_TYPE_SMS = "SMS";
27
+ public static String MSG_WIDGET_TYPE_HELP_PRMOPT = "HELP_PROMPT";
28
+ public static String MSG_WIDGET_TYPE_SEARCH_CONTACT = "SEARCH_CONTACT";
29
+
30
+ /**
31
+ * okhttp server url
32
+ */
33
+
34
+ public static String SERVER_URL = "https://smartphone.herokuapp.com/";
35
+ public static long CUSTOM_TIMEOUT = 120;
36
+
37
+ /**
38
+ * ImagePickerType
39
+ */
40
+ public static String PICKERTYPE_IMAGE_UPLOAD = "image_upload";
41
+ public static String PICKERTYPE_IMAGE_PICK = "image_picker";
42
+
43
+ /**
44
+ * for send OkHttp3Request with json format
45
+ */
46
+ public static MediaType JSON = MediaType.parse("application/json; charset=utf-8");
47
+ public static String POST = "post";
48
+ public static String GET = "get";
49
+
50
+ public static String TAG = "risingandroid";
51
+
52
+ public static String LOADING_ASKING_TO_GPT = "Asking To GPT";
53
+ public static String LOADING_UPLOADING_IAMGE = "Uploading Image";
54
+ public static String LOADING_ANALYZING_IMAGE = "Analyzing Image";
55
+ public static String LOADING_DOWNLOADING_IMAGE = "Downloading Image";
56
+
57
+ public static String HELP_COMMAND_ERROR_NO_MAIN = "no main command";
58
+ public static String HELP_COMMAND_ERROR_NO_INVALID_FORMAT = "Invalid Command Format";
59
+ public static String HELP_COMMAND = "help";
60
+ public static String HELP_COMMAND_ALL = "all";
61
+
62
+ public static String ERROR_MSG_JSON = "JSON Invalid Format";
63
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/ImageHelper.java ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.utils;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.Paint;
6
+ import android.graphics.PorterDuffXfermode;
7
+ import android.graphics.Rect;
8
+ import android.graphics.RectF;
9
+ import android.graphics.Bitmap.Config;
10
+ import android.graphics.PorterDuff.Mode;
11
+
12
+ /**
13
+ * static functions for image process
14
+ */
15
+ public class ImageHelper {
16
+
17
+ /**
18
+ * this function convert original bitmap to rounded bitmap
19
+ *
20
+ * @param bitmap bitmap to make conner round
21
+ * @param pixels conner round pixel size
22
+ * @return rounded bitmap file conner-rounded with given pixels
23
+ */
24
+ public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int pixels) {
25
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
26
+ Canvas canvas = new Canvas(output);
27
+
28
+ final int color = 0xff424242;
29
+ final Paint paint = new Paint();
30
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
31
+ final RectF rectF = new RectF(rect);
32
+ final float roundPx = pixels;
33
+
34
+ paint.setAntiAlias(true);
35
+ canvas.drawARGB(0, 0, 0, 0);
36
+ paint.setColor(color);
37
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
38
+
39
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
40
+ canvas.drawBitmap(bitmap, rect, rect, paint);
41
+
42
+ return output;
43
+ }
44
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/ReqType.kt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.utils
2
+
3
+ class ReqType {
4
+ val MESSAGE = "message"
5
+ val IMAGE_UPLOAD = "image_upload"
6
+
7
+ companion object {
8
+ var instance: ReqType = ReqType()
9
+ }
10
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/utils/Utils.kt ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.ContentResolver
5
+ import android.content.ContentUris
6
+ import android.content.Context
7
+ import android.database.Cursor
8
+ import android.graphics.Bitmap
9
+ import android.graphics.BitmapFactory
10
+ import android.net.Uri
11
+ import android.os.Environment
12
+ import android.provider.ContactsContract
13
+ import android.provider.MediaStore
14
+ import android.widget.ImageView
15
+ import androidx.room.RoomDatabase
16
+ import com.bumptech.glide.Glide
17
+ import com.matthaigh27.chatgptwrapper.MyApplication
18
+ import com.matthaigh27.chatgptwrapper.R
19
+ import com.matthaigh27.chatgptwrapper.database.MyDatabase
20
+ import com.matthaigh27.chatgptwrapper.database.entity.ContactEntity
21
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
22
+ import com.matthaigh27.chatgptwrapper.models.common.HelpCommandModel
23
+ import com.matthaigh27.chatgptwrapper.models.common.HelpPromptModel
24
+ import com.matthaigh27.chatgptwrapper.services.api.HttpClient
25
+ import com.matthaigh27.chatgptwrapper.utils.Constants.*
26
+ import kotlinx.coroutines.CoroutineScope
27
+ import kotlinx.coroutines.Dispatchers
28
+ import kotlinx.coroutines.async
29
+ import kotlinx.coroutines.launch
30
+ import org.json.JSONArray
31
+ import org.json.JSONException
32
+ import org.json.JSONObject
33
+ import java.io.ByteArrayOutputStream
34
+ import java.io.File
35
+ import java.io.FileInputStream
36
+ import java.io.FileNotFoundException
37
+ import java.io.IOException
38
+ import java.io.InputStream
39
+ import java.net.HttpURLConnection
40
+ import java.net.URL
41
+ import java.text.SimpleDateFormat
42
+ import java.util.Date
43
+ import java.util.Locale
44
+
45
+ class Utils {
46
+ /**
47
+ * set bitmap from download url generated by firebase store
48
+ *
49
+ * @param src download url of image
50
+ * @return bitmap of the image
51
+ */
52
+ fun getBitmapFromURL(src: String?): Bitmap? {
53
+ return try {
54
+ val url = URL(src)
55
+ val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
56
+ connection.doInput = true
57
+ connection.connect()
58
+ val input: InputStream = connection.inputStream
59
+ BitmapFactory.decodeStream(input)
60
+ } catch (e: Exception) {
61
+ e.printStackTrace()
62
+ null
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param uri uri for converting to ByteArray
68
+ * @return ByteArray data converted from image uri
69
+ */
70
+ fun convertImageToByte(uri: Uri?): ByteArray? {
71
+ var data: ByteArray? = null
72
+ try {
73
+ val cr = MyApplication.appContext.contentResolver
74
+ val inputStream: InputStream? = cr.openInputStream(uri!!)
75
+ val bitmap = BitmapFactory.decodeStream(inputStream)
76
+ val baos = ByteArrayOutputStream()
77
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
78
+ data = baos.toByteArray()
79
+ } catch (e: FileNotFoundException) {
80
+ e.printStackTrace()
81
+ }
82
+ return data
83
+ }
84
+
85
+ /**
86
+ * @param path local path for converting to ByteArray
87
+ * @return ByteArray data converted from image local path
88
+ */
89
+ @Suppress("UNREACHABLE_CODE")
90
+ fun getBytesFromPath(path: String?): ByteArray {
91
+ var byteArray: ByteArray? = null
92
+ try {
93
+ val stream = FileInputStream(path)
94
+ byteArray = stream.readBytes()
95
+ stream.close()
96
+ return byteArray
97
+ } catch (e: IOException) {
98
+ throw Exception(e)
99
+ }
100
+ return byteArray
101
+ }
102
+
103
+ /**
104
+ * this creats image file with current datetime captured by camera
105
+ */
106
+ fun createSDCardFile(): File {
107
+ val timeStamp: String =
108
+ SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
109
+ val imageFileName = "IMG_" + timeStamp + "_"
110
+ val storageDir: File =
111
+ MyApplication.appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
112
+ return File.createTempFile(
113
+ imageFileName, /* prefix */
114
+ ".jpg", /* suffix */
115
+ storageDir /* directory */
116
+ )
117
+ }
118
+
119
+ /**
120
+ * when users input command starting with '/'
121
+ *
122
+ * @param strCommand is string with '/' and '-', ex: /browser -d
123
+ * @return command model with main ommand and assistance command
124
+ */
125
+ fun getHelpCommandFromStr(strCommand: String): HelpCommandModel {
126
+ val commandModel = HelpCommandModel()
127
+ if (strCommand == "/$HELP_COMMAND") {
128
+ commandModel.mainCommandName = HELP_COMMAND
129
+ commandModel.assistCommandName = HELP_COMMAND_ALL
130
+ return commandModel
131
+ }
132
+ try {
133
+ if (strCommand.startsWith("/$HELP_COMMAND") || strCommand.startsWith("!$HELP_COMMAND")) {
134
+ val words = strCommand.split("\\s".toRegex()).toTypedArray()
135
+ if (words.size != 2) {
136
+ throw Exception(HELP_COMMAND_ERROR_NO_MAIN)
137
+ }
138
+ commandModel.mainCommandName = words[0].substring(1, words[0].length)
139
+ commandModel.assistCommandName = words[1]
140
+ if (commandModel.mainCommandName == "" || commandModel.assistCommandName == "") {
141
+ throw Exception(HELP_COMMAND_ERROR_NO_MAIN)
142
+ }
143
+ } else {
144
+ commandModel.mainCommandName = strCommand.substring(1, strCommand.length)
145
+ }
146
+ } catch (e: Exception) {
147
+ throw Exception(HELP_COMMAND_ERROR_NO_INVALID_FORMAT)
148
+ }
149
+ return commandModel
150
+ }
151
+
152
+ fun getHelpCommandListFromJsonString(jsonStrCommandList: String): ArrayList<HelpPromptModel> {
153
+ val commandlist = ArrayList<HelpPromptModel>()
154
+ try {
155
+ val helpCommandStrList = JSONArray(jsonStrCommandList)
156
+ for (i in 0 until helpCommandStrList.length()) {
157
+ val helpCommand = JSONObject(helpCommandStrList[i].toString())
158
+
159
+ val helpPromptModel = HelpPromptModel()
160
+ helpPromptModel.name = helpCommand.getString("name")
161
+ helpPromptModel.description = helpCommand.getString("description")
162
+ helpPromptModel.prompt = helpCommand.getString("prompt")
163
+
164
+ helpPromptModel.tags = ArrayList()
165
+ val jsonArrayTags = helpCommand.getJSONArray("tags")
166
+ for (j in 0 until jsonArrayTags.length()) {
167
+ helpPromptModel.tags!!.add(jsonArrayTags[j].toString())
168
+ }
169
+ commandlist.add(helpPromptModel)
170
+ }
171
+ } catch (e: JSONException) {
172
+ e.printStackTrace()
173
+ throw Exception(ERROR_MSG_JSON)
174
+ }
175
+ return commandlist
176
+ }
177
+
178
+ @SuppressLint("Range")
179
+ fun getContacts(context: Context): ArrayList<ContactModel> {
180
+ val resolver: ContentResolver = context.contentResolver;
181
+ val cursor = resolver.query(
182
+ ContactsContract.Contacts.CONTENT_URI, null, null, null, null
183
+ )
184
+
185
+ val contacts = ArrayList<ContactModel>()
186
+ if (cursor!!.count > 0) {
187
+ while (cursor.moveToNext()) {
188
+ val id = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID))
189
+
190
+ val name =
191
+ cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
192
+ val phoneNumber = (cursor.getString(
193
+ cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)
194
+ )).toInt()
195
+
196
+ val contact = ContactModel()
197
+ contact.id = id
198
+ contact.name = name
199
+
200
+ if (phoneNumber > 0) {
201
+ val cursorPhone = context.contentResolver.query(
202
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
203
+ null,
204
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=?",
205
+ arrayOf(id),
206
+ null
207
+ )
208
+
209
+ if (cursorPhone!!.count > 0) {
210
+ while (cursorPhone.moveToNext()) {
211
+ val phoneNumValue = cursorPhone.getString(
212
+ cursorPhone.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
213
+ )
214
+ contact.phoneList!!.add(phoneNumValue)
215
+ }
216
+ }
217
+ cursorPhone.close()
218
+ }
219
+
220
+ contacts.add(contact)
221
+ }
222
+ }
223
+ cursor.close()
224
+ return contacts
225
+ }
226
+
227
+ suspend fun getChangedContacts(
228
+ contacts: ArrayList<ContactModel>,
229
+ roomDatabaseHandler: MyDatabase
230
+ ): ArrayList<ContactModel> {
231
+ return CoroutineScope(Dispatchers.IO).async {
232
+ val originalContacts = roomDatabaseHandler.contactDao().getAllContacts()
233
+ val changedContactList = ArrayList<ContactModel>()
234
+ for (i in originalContacts.indices) {
235
+ var isExist = false
236
+ contacts.forEach { contact ->
237
+ if (originalContacts[i].id == contact.id) {
238
+ if (originalContacts[i].name != contact.name ||
239
+ originalContacts[i].phoneNumber != contact.phoneList.toString()
240
+ ) {
241
+ contact.status = "updated"
242
+ changedContactList.add(contact)
243
+
244
+ try {
245
+ roomDatabaseHandler.contactDao().updateContact(
246
+ ContactEntity(
247
+ contact.id,
248
+ contact.name,
249
+ contact.phoneList.toString()
250
+ )
251
+ )
252
+ } catch (e: Exception) {
253
+ e.printStackTrace()
254
+ }
255
+ } else {
256
+ contact.status = "nothing"
257
+ }
258
+ isExist = true
259
+ return@forEach
260
+ }
261
+ }
262
+ if (!isExist) {
263
+ val deletedContacts = ContactModel()
264
+ deletedContacts.id = originalContacts[i].id
265
+ deletedContacts.status = "deleted"
266
+ changedContactList.add(deletedContacts)
267
+
268
+ try {
269
+ roomDatabaseHandler.contactDao().deleteContact(
270
+ ContactEntity(
271
+ deletedContacts.id,
272
+ deletedContacts.name,
273
+ deletedContacts.phoneList.toString()
274
+ )
275
+ )
276
+ } catch (e: Exception) {
277
+ e.printStackTrace()
278
+ }
279
+ }
280
+ }
281
+ contacts.forEach { contact ->
282
+ if (contact.status.isEmpty()) {
283
+ contact.status = "created"
284
+ changedContactList.add(contact)
285
+ try {
286
+ roomDatabaseHandler.contactDao().insertContact(
287
+ ContactEntity(
288
+ contact.id,
289
+ contact.name,
290
+ contact.phoneList.toString()
291
+ )
292
+ )
293
+ } catch (e: Exception) {
294
+ e.printStackTrace()
295
+ }
296
+ }
297
+ }
298
+ changedContactList
299
+ }.await()
300
+ }
301
+
302
+ fun getContactModelById(contactId: String, contacts: ArrayList<ContactModel>): ContactModel {
303
+ var contactModel = ContactModel()
304
+ contacts.forEach { contact ->
305
+ if (contactId == contact.id) {
306
+ return contact
307
+ }
308
+ }
309
+ return contactModel
310
+ }
311
+
312
+ fun setContactAvatar(contactId: Long, context: Context, imageView: ImageView) {
313
+ val uri = ContentUris.withAppendedId(
314
+ ContactsContract.Contacts.CONTENT_URI, contactId
315
+ )
316
+
317
+ Glide.with(context)
318
+ .load(uri)
319
+ .placeholder(R.drawable.default_avatar) // Set placeholder image
320
+ .error(R.drawable.default_avatar) // Set error image
321
+ .fallback(R.drawable.default_avatar) // Set fallback image
322
+ .into(imageView)
323
+ }
324
+
325
+ fun convertContactModelToJsonArray(contacts: ArrayList<ContactModel>): JSONArray {
326
+ var jsonContacts = JSONArray()
327
+ contacts.forEach { contact ->
328
+ val jsonObjectContact = JSONObject()
329
+ jsonObjectContact.put("contactId", contact.id)
330
+ jsonObjectContact.put("displayName", contact.name)
331
+ jsonObjectContact.put("phoneNumbers", contact.phoneList)
332
+ jsonObjectContact.put("status", contact.status)
333
+
334
+ jsonContacts.put(jsonObjectContact)
335
+ }
336
+ return jsonContacts
337
+ }
338
+
339
+ fun getRealPathFromUri(context: Context, contentUri: Uri?): String? {
340
+ var cursor: Cursor? = null
341
+ return try {
342
+ val proj = arrayOf(MediaStore.Images.Media.DATA)
343
+ cursor = context.contentResolver.query(contentUri!!, proj, null, null, null)
344
+ val column_index: Int = cursor!!.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
345
+ cursor.moveToFirst()
346
+ cursor.getString(column_index)
347
+ } finally {
348
+ cursor?.close()
349
+ }
350
+ }
351
+
352
+
353
+ companion object {
354
+ var instance: Utils = Utils()
355
+ }
356
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ContactDetailItem.kt ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.content.ContentUris
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.pm.PackageManager
7
+ import android.net.Uri
8
+ import android.provider.ContactsContract
9
+ import android.telecom.VideoProfile
10
+ import android.util.AttributeSet
11
+ import android.view.LayoutInflater
12
+ import android.view.View
13
+ import android.widget.ImageView
14
+ import android.widget.TextView
15
+ import androidx.constraintlayout.widget.ConstraintLayout
16
+ import com.matthaigh27.chatgptwrapper.R
17
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
18
+
19
+ class ContactDetailItem(
20
+ context: Context, attrs: AttributeSet? = null
21
+ ) : ConstraintLayout(context, attrs), View.OnClickListener {
22
+ private lateinit var mTvPhoneNumber: TextView
23
+ private lateinit var mTvPhoneType: TextView
24
+ private var mPhoneNumber: String = ""
25
+ private var mUserName: String = ""
26
+
27
+ private var mContext = context
28
+ private var mListener: OnSMSClickListener? = null
29
+ private var mContactDetailVisibilityListener: OnContactDetailVisibilityListener? = null
30
+
31
+ init {
32
+ initView()
33
+ }
34
+
35
+ fun setOnSMSClickListener(listener: OnSMSClickListener) {
36
+ mListener = listener
37
+ }
38
+
39
+ fun setOnContactDetailVisibilityListener(listener: OnContactDetailVisibilityListener) {
40
+ mContactDetailVisibilityListener = listener
41
+ }
42
+
43
+ private fun initView() {
44
+ LayoutInflater.from(context).inflate(R.layout.item_contact_detail, this, true)
45
+
46
+ mTvPhoneNumber = findViewById(R.id.tv_phone_number)
47
+ mTvPhoneType = findViewById(R.id.tv_phone_type)
48
+
49
+ findViewById<ImageView>(R.id.btn_voice_call).setOnClickListener(this)
50
+ findViewById<ImageView>(R.id.btn_send_message).setOnClickListener(this)
51
+ }
52
+
53
+ fun setContactDetailItemInfo(phoneNumber: String, username: String) {
54
+ mPhoneNumber = phoneNumber
55
+ mUserName = username
56
+ mTvPhoneNumber.text = phoneNumber
57
+ }
58
+
59
+ override fun onClick(view: View?) {
60
+ when (view!!.id) {
61
+ R.id.btn_voice_call -> {
62
+ mContactDetailVisibilityListener!!.invisible()
63
+ mListener!!.onVoiceCallListener(mPhoneNumber, mUserName)
64
+ doVoiceCall(mPhoneNumber)
65
+ }
66
+
67
+ R.id.btn_send_message -> {
68
+ mListener!!.onSMSClickListener(mPhoneNumber)
69
+ mContactDetailVisibilityListener!!.invisible()
70
+ }
71
+ }
72
+ }
73
+
74
+ private fun doVoiceCall(phoneNumber: String) {
75
+ val callIntent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$phoneNumber"))
76
+ mContext.startActivity(callIntent)
77
+ return
78
+ }
79
+
80
+ fun openMessagingApp(phoneNumber: String, message: String) {
81
+ val uri = Uri.parse("sms:$phoneNumber")
82
+ val intent = Intent(Intent.ACTION_VIEW, uri).apply {
83
+ putExtra("sms_body", message)
84
+ }
85
+ mContext.startActivity(intent)
86
+ }
87
+
88
+ private fun doVideoCall(phoneNumber: String) {
89
+ val callIntent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
90
+ callIntent.putExtra(
91
+ "android.telecom.extra.START_CALL_WITH_VIDEO_STATE",
92
+ VideoProfile.STATE_BIDIRECTIONAL
93
+ )
94
+ val hasCamera =
95
+ context.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) ?: false
96
+ val hasTelephony =
97
+ context.packageManager?.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) ?: false
98
+ val canMakeVideoCalls = hasCamera && hasTelephony
99
+
100
+ if (canMakeVideoCalls) {
101
+ context.startActivity(callIntent)
102
+ }
103
+ }
104
+
105
+ interface OnSMSClickListener {
106
+ fun onSMSClickListener(phoneNumber: String)
107
+ fun onVoiceCallListener(phoneNumber: String, toName: String)
108
+ }
109
+
110
+ interface OnContactDetailVisibilityListener {
111
+ fun invisible()
112
+ }
113
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ContactDetailWidget.kt ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.content.ContentUris
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.provider.ContactsContract
7
+ import android.util.AttributeSet
8
+ import android.view.LayoutInflater
9
+ import android.view.View
10
+ import android.widget.ImageView
11
+ import android.widget.LinearLayout
12
+ import android.widget.TextView
13
+ import androidx.constraintlayout.widget.ConstraintLayout
14
+ import androidx.core.view.forEach
15
+ import com.google.android.material.bottomsheet.BottomSheetDialog
16
+ import com.matthaigh27.chatgptwrapper.R
17
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
18
+ import com.matthaigh27.chatgptwrapper.utils.Utils
19
+
20
+ class ContactDetailWidget(
21
+ context: Context, contactModel: ContactModel, smsClickListener: ContactDetailItem.OnSMSClickListener
22
+ ) : BottomSheetDialog(context), View.OnClickListener {
23
+
24
+ private lateinit var mAvatar: ImageView
25
+ private lateinit var mDisplayName: TextView
26
+ private lateinit var mBtnEditContact: ImageView
27
+ private lateinit var mLlPhones: LinearLayout
28
+
29
+ private var mContactModel = contactModel
30
+
31
+ private var mOnSMSClickListener: ContactDetailItem.OnSMSClickListener = smsClickListener
32
+
33
+ init {
34
+ initView()
35
+ }
36
+
37
+
38
+ private fun initView() {
39
+ setContentView(R.layout.view_contact_detail)
40
+
41
+ mDisplayName = findViewById(R.id.tv_displayname)!!
42
+ mBtnEditContact = findViewById(R.id.btn_edit_contact)!!
43
+ mLlPhones = findViewById(R.id.ll_contacts)!!
44
+
45
+ mLlPhones.removeAllViews()
46
+ mDisplayName.text = mContactModel.name
47
+ mContactModel.phoneList!!.forEach { phoneNumber ->
48
+ val contactDetailItem = ContactDetailItem(context)
49
+ contactDetailItem.setContactDetailItemInfo(phoneNumber, mContactModel.name)
50
+ contactDetailItem.setOnSMSClickListener(mOnSMSClickListener)
51
+ contactDetailItem.setOnContactDetailVisibilityListener(object:
52
+ ContactDetailItem.OnContactDetailVisibilityListener {
53
+ override fun invisible() {
54
+ this@ContactDetailWidget.dismiss()
55
+ }
56
+ })
57
+ mLlPhones.addView(contactDetailItem)
58
+ }
59
+
60
+ mBtnEditContact.setOnClickListener(this)
61
+ mAvatar = findViewById(R.id.iv_avatar)!!
62
+ Utils.instance.setContactAvatar(mContactModel.id.toLong(), context, mAvatar)
63
+ }
64
+
65
+ override fun onClick(view: View?) {
66
+ when (view!!.id) {
67
+ R.id.btn_edit_contact -> {
68
+ goToContactEditor(mContactModel.id)
69
+ }
70
+ R.id.btn_send_message -> {
71
+ }
72
+ }
73
+ }
74
+
75
+ private fun goToContactEditor(contactId: String) {
76
+ val editIntent = Intent(Intent.ACTION_EDIT)
77
+ val contactUri =
78
+ ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId.toLong())
79
+ editIntent.setData(contactUri)
80
+ context.startActivity(editIntent)
81
+ }
82
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/HelpCommandEditText.kt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.view.ViewGroup
6
+ import android.widget.EditText
7
+ import android.widget.LinearLayout
8
+ import androidx.annotation.Dimension
9
+ import com.matthaigh27.chatgptwrapper.R
10
+
11
+ @SuppressLint("AppCompatCustomView")
12
+ class HelpCommandEditText(context: Context) : EditText(context) {
13
+ init {
14
+ val layoutParams: LinearLayout.LayoutParams = LinearLayout.LayoutParams(
15
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
16
+ )
17
+ val marginBottom =
18
+ context.resources.getDimensionPixelSize(R.dimen.view_help_prompt_key_edittext_margin_bottom)
19
+ layoutParams.setMargins(0, 0, 0, marginBottom)
20
+ this.layoutParams = layoutParams
21
+
22
+ minHeight =
23
+ context.resources.getDimensionPixelSize(R.dimen.view_help_prompt_key_edittext_minheight)
24
+
25
+ val paddingStart =
26
+ context.resources.getDimensionPixelSize(R.dimen.view_help_prompt_edittext_padding_start)
27
+ setPadding(paddingStart, 0, 0, 0)
28
+
29
+
30
+ setTextSize(
31
+ Dimension.DP,
32
+ context.resources.getDimensionPixelSize(R.dimen.view_help_prompt_edittext_fontsize)
33
+ .toFloat()
34
+ )
35
+ setTextColor(context.getColor(R.color.primary))
36
+ setHintTextColor(context.getColor(R.color.view_help_prompt_edittext_hint_color))
37
+ background = context.getDrawable(R.drawable.background_view_help_prompt_edittext)
38
+ }
39
+
40
+ fun initView(keyName: String) {
41
+ hint = keyName
42
+ tag = keyName
43
+ }
44
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/HelpPromptWidget.kt ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.app.Dialog
4
+ import android.content.Context
5
+ import android.os.Bundle
6
+ import android.util.Log
7
+ import android.view.View
8
+ import android.view.ViewGroup
9
+ import android.view.Window
10
+ import android.widget.ArrayAdapter
11
+ import android.widget.AutoCompleteTextView
12
+ import android.widget.Button
13
+ import android.widget.EditText
14
+ import android.widget.LinearLayout
15
+ import android.widget.TextView
16
+ import androidx.constraintlayout.widget.ConstraintLayout
17
+ import com.matthaigh27.chatgptwrapper.R
18
+ import com.matthaigh27.chatgptwrapper.models.common.HelpPromptModel
19
+
20
+ class HelpPromptWidget(context: Context, model: HelpPromptModel) : ConstraintLayout(context),
21
+ View.OnClickListener {
22
+ private lateinit var mListener: OnHelpPromptListener
23
+ private lateinit var mLlPromptKeys: LinearLayout
24
+ private lateinit var mTvKeysTitle: TextView
25
+
26
+ private var mListEtPrompt: ArrayList<EditText>? = null
27
+
28
+ val mModel: HelpPromptModel = model
29
+
30
+ init {
31
+ initView()
32
+ }
33
+
34
+ fun setOnClickListener(listener: OnHelpPromptListener) {
35
+ mListener = listener
36
+ }
37
+
38
+ private fun initView() {
39
+ inflate(context, R.layout.view_help_prompt, this)
40
+
41
+ layoutParams = LayoutParams(
42
+ ViewGroup.LayoutParams.MATCH_PARENT,
43
+ ViewGroup.LayoutParams.WRAP_CONTENT
44
+ )
45
+
46
+ mLlPromptKeys = findViewById(R.id.ll_prompt_keys)
47
+ mTvKeysTitle = findViewById(R.id.tv_keys_title)
48
+ mTvKeysTitle.setText(mModel.name)
49
+
50
+ findViewById<Button>(R.id.btn_ok).setOnClickListener(this)
51
+ findViewById<Button>(R.id.btn_cancel).setOnClickListener(this)
52
+
53
+ initPromptList()
54
+
55
+ }
56
+
57
+ private fun initPromptList() {
58
+ if (mModel.tags!!.size > 0) {
59
+ mLlPromptKeys.removeAllViews()
60
+ mListEtPrompt = ArrayList()
61
+
62
+ for (i in 0 until mModel.tags!!.size) {
63
+ val etKey = HelpCommandEditText(context)
64
+ etKey.initView(mModel.tags!![i])
65
+ mLlPromptKeys.addView(etKey)
66
+ mListEtPrompt!!.add(etKey)
67
+ }
68
+ }
69
+ }
70
+
71
+ override fun onClick(view: View?) {
72
+ when (view?.id) {
73
+ R.id.btn_ok -> {
74
+ outputCompletePrompt()
75
+ }
76
+ R.id.btn_cancel -> {
77
+ mListener.onCancel()
78
+ }
79
+ }
80
+ }
81
+
82
+ private fun outputCompletePrompt() {
83
+ var promptTemplate = mModel.prompt
84
+ if(mModel.tags!!.size > 0) {
85
+ mListEtPrompt!!.forEach { etKeyPrompt ->
86
+ val prompt = etKeyPrompt.text.toString()
87
+ if(prompt.isEmpty()) return
88
+ promptTemplate = promptTemplate.replace(etKeyPrompt.tag.toString(), prompt)
89
+ }
90
+ }
91
+ mListener.onSuccess(promptTemplate)
92
+ }
93
+
94
+ interface OnHelpPromptListener {
95
+ fun onSuccess(prompt: String)
96
+ fun onCancel()
97
+ }
98
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/ImagePickerWidget.kt ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import android.view.View.OnClickListener
6
+ import android.view.ViewGroup
7
+ import android.widget.LinearLayout
8
+ import androidx.constraintlayout.widget.ConstraintLayout
9
+ import com.matthaigh27.chatgptwrapper.R
10
+
11
+
12
+ class ImagePickerWidget(context: Context) : LinearLayout(context), OnClickListener {
13
+
14
+ private lateinit var mClickListener: OnPositiveButtonClickListener
15
+
16
+ init {
17
+ initView()
18
+ }
19
+
20
+ fun setOnClickListener(listener: OnPositiveButtonClickListener) {
21
+ mClickListener = listener
22
+ }
23
+
24
+ private fun initView() {
25
+ inflate(context, R.layout.view_image_picker, this)
26
+
27
+ layoutParams = ConstraintLayout.LayoutParams(
28
+ ViewGroup.LayoutParams.MATCH_PARENT,
29
+ ViewGroup.LayoutParams.WRAP_CONTENT
30
+ )
31
+
32
+ findViewById<LinearLayout>(R.id.lytCameraPick).setOnClickListener(this)
33
+ findViewById<LinearLayout>(R.id.lytGalleryPick).setOnClickListener(this)
34
+ }
35
+
36
+ override fun onClick(view: View?) {
37
+ when (view?.id) {
38
+ R.id.lytCameraPick -> {
39
+ mClickListener.onPositiveBtnClick(true)
40
+ }
41
+ R.id.lytGalleryPick -> {
42
+ mClickListener.onPositiveBtnClick(false)
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * callback function invoked when filepickerdialog buttons are pressed
49
+ */
50
+ interface OnPositiveButtonClickListener {
51
+ fun onPositiveBtnClick(isCamera: Boolean?)
52
+ }
53
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/SearchContactWidget.kt ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.content.ContentUris
4
+ import android.content.Context
5
+ import android.net.Uri
6
+ import android.provider.ContactsContract
7
+ import android.util.AttributeSet
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.widget.ImageView
11
+ import android.widget.LinearLayout
12
+ import android.widget.TextView
13
+ import androidx.constraintlayout.widget.ConstraintLayout
14
+ import com.bumptech.glide.Glide
15
+ import com.google.android.material.bottomsheet.BottomSheetDialog
16
+ import com.matthaigh27.chatgptwrapper.R
17
+ import com.matthaigh27.chatgptwrapper.models.common.ContactModel
18
+ import com.matthaigh27.chatgptwrapper.utils.Utils
19
+ import de.hdodenhof.circleimageview.CircleImageView
20
+
21
+
22
+ class SearchContactWidget(
23
+ context: Context, cotactModel: ContactModel, attrs: AttributeSet?
24
+ ) : ConstraintLayout(context, attrs), View.OnClickListener {
25
+
26
+ private val mContext: Context
27
+
28
+ private val civInfoAvatar: CircleImageView
29
+ private val tvInfoName: TextView
30
+
31
+ private var mContact:ContactModel = cotactModel
32
+ var mSMSOnClickListener: ContactDetailItem.OnSMSClickListener? = null
33
+
34
+ init {
35
+ inflate(context, R.layout.view_search_contact, this)
36
+ mContext = context
37
+
38
+ layoutParams = LayoutParams(
39
+ ViewGroup.LayoutParams.WRAP_CONTENT,
40
+ ViewGroup.LayoutParams.WRAP_CONTENT
41
+ )
42
+
43
+ civInfoAvatar = findViewById(R.id.civ_avatar)
44
+ tvInfoName = findViewById(R.id.tv_info_name)
45
+
46
+ tvInfoName.text = mContact.name
47
+ Utils.instance.setContactAvatar(mContact.id.toLong(), mContext, civInfoAvatar)
48
+
49
+ this.setOnClickListener(this)
50
+ }
51
+
52
+
53
+
54
+ private fun showContactDetailView() {
55
+ val bottomSheetDialog = ContactDetailWidget(mContext, mContact, mSMSOnClickListener!!)
56
+ bottomSheetDialog.show()
57
+ }
58
+
59
+ override fun onClick(view: View?) {
60
+ showContactDetailView()
61
+ }
62
+ }
Android/app/src/main/java/com/matthaigh27/chatgptwrapper/widgets/SmsEditorWidget.kt ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.matthaigh27.chatgptwrapper.widgets
2
+
3
+ import android.content.Context
4
+ import android.util.AttributeSet
5
+ import android.view.View
6
+ import android.widget.Button
7
+ import android.widget.EditText
8
+ import android.widget.Toast
9
+ import androidx.constraintlayout.widget.ConstraintLayout
10
+ import com.matthaigh27.chatgptwrapper.R
11
+
12
+
13
+ class SmsEditorWidget(
14
+ context: Context, attrs: AttributeSet?
15
+ ) : ConstraintLayout(context, attrs) {
16
+
17
+ private val mContext: Context
18
+
19
+ private val etToName: EditText
20
+ private val etMessage: EditText
21
+
22
+ private val btnConfirm: Button
23
+ private val btnCancel: Button
24
+
25
+ private var mListener: OnClickListener? = null
26
+
27
+ init {
28
+ inflate(context, R.layout.view_sms_editor, this)
29
+ mContext = context
30
+
31
+ layoutParams = LayoutParams(
32
+ android.view.ViewGroup.LayoutParams.MATCH_PARENT,
33
+ android.view.ViewGroup.LayoutParams.WRAP_CONTENT
34
+ )
35
+
36
+ etToName = findViewById(R.id.et_to_name)
37
+ etMessage = findViewById(R.id.et_message)
38
+
39
+ btnConfirm = findViewById(R.id.btn_confirm)
40
+ btnCancel = findViewById(R.id.btn_cancel)
41
+
42
+ btnConfirm.setOnClickListener {
43
+ if (etToName.text.toString().isEmpty() || etMessage.text.toString().isEmpty()) {
44
+ Toast.makeText(
45
+ mContext, "Please input phone number and message.", Toast.LENGTH_SHORT
46
+ ).show()
47
+ return@setOnClickListener
48
+ }
49
+ mListener!!.confirmSMS(etToName.text.toString(), etMessage.text.toString())
50
+ hide()
51
+ }
52
+
53
+ btnCancel.setOnClickListener {
54
+ hide()
55
+ mListener!!.cancelSMS()
56
+ }
57
+ }
58
+
59
+ fun setToUserName(name: String) {
60
+ etToName.setText(name)
61
+ }
62
+
63
+ fun hide() {
64
+ this.visibility = View.GONE
65
+ etToName.setText("")
66
+ etMessage.setText("")
67
+ }
68
+
69
+ fun setOnClickListener(listener: OnClickListener) {
70
+ mListener = listener
71
+ }
72
+
73
+ interface OnClickListener {
74
+ fun confirmSMS(phonenumber: String, message: String);
75
+ fun cancelSMS();
76
+ }
77
+ }
Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ xmlns:aapt="http://schemas.android.com/aapt"
3
+ android:width="108dp"
4
+ android:height="108dp"
5
+ android:viewportWidth="108"
6
+ android:viewportHeight="108">
7
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
8
+ <aapt:attr name="android:fillColor">
9
+ <gradient
10
+ android:endX="85.84757"
11
+ android:endY="92.4963"
12
+ android:startX="42.9492"
13
+ android:startY="49.59793"
14
+ android:type="linear">
15
+ <item
16
+ android:color="#44000000"
17
+ android:offset="0.0" />
18
+ <item
19
+ android:color="#00000000"
20
+ android:offset="1.0" />
21
+ </gradient>
22
+ </aapt:attr>
23
+ </path>
24
+ <path
25
+ android:fillColor="#FFFFFF"
26
+ android:fillType="nonZero"
27
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
28
+ android:strokeWidth="1"
29
+ android:strokeColor="#00000000" />
30
+ </vector>
Android/app/src/main/res/drawable/background_common_rounded_button.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="@color/common_rounded_button_bgcolor" />
5
+ <corners android:radius="@dimen/control_button_common_radius" />
6
+ </shape>
Android/app/src/main/res/drawable/background_content_top.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="@color/chat_content_top_bgcolor" />
5
+ <corners
6
+ android:bottomLeftRadius="@dimen/chat_content_top_radius"
7
+ android:bottomRightRadius="@dimen/chat_content_top_radius" />
8
+ </shape>
Android/app/src/main/res/drawable/background_control_button_common.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="@color/control_button_common_bgcolor" />
5
+ <corners android:radius="@dimen/control_button_common_radius" />
6
+ </shape>
Android/app/src/main/res/drawable/background_dialog_common_confirm.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="@color/dialog_common_confirm_bg_color" />
5
+ <corners android:radius="@dimen/dialog_common_confirm_radius" />
6
+ </shape>
Android/app/src/main/res/drawable/background_feedback_layout.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <stroke
5
+ android:width="@dimen/chat_feedback_stroke_width"
6
+ android:color="@color/feedback_common_color" />
7
+ <corners android:radius="@dimen/chat_feedback_radius" />
8
+ </shape>
Android/app/src/main/res/drawable/background_load_assets_button_cancel.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="@color/load_assets_button_bgcolor" />
5
+ <corners android:radius="@dimen/load_assets_button_radius" />
6
+ </shape>