├── .github └── workflows │ └── android-test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .editorconfig ├── .gitignore ├── build.gradle ├── ktlint.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── leti │ │ └── phonedetector │ │ └── UITest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_empty_user-web.png │ ├── ic_launcher-web.png │ ├── ic_spam-web.png │ ├── ic_spam_image-web.png │ ├── java │ │ └── com │ │ │ └── leti │ │ │ └── phonedetector │ │ │ ├── DataAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── OverlayActivity.kt │ │ │ ├── PhoneStateReceiver.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── StatisticsActivity.kt │ │ │ ├── api │ │ │ ├── GetContact │ │ │ │ ├── AESCipher.kt │ │ │ │ ├── ConfigUpdater.kt │ │ │ │ ├── GetContactAPI.kt │ │ │ │ ├── Requester.kt │ │ │ │ └── config.kt │ │ │ └── NeberitrubkuAPI │ │ │ │ └── NeberitrubkuAPI.kt │ │ │ ├── bitmap │ │ │ └── BitmapReader.kt │ │ │ ├── config.kt │ │ │ ├── contacts │ │ │ └── Contacts.kt │ │ │ ├── database │ │ │ ├── DBContract.kt │ │ │ ├── PhoneLogDBHelper.kt │ │ │ └── TokenDBHelper.kt │ │ │ ├── model │ │ │ ├── PhoneInfo.kt │ │ │ └── Token.kt │ │ │ ├── notification │ │ │ ├── BlockNotification.kt │ │ │ ├── IncomingNotification.kt │ │ │ └── NotificationPublisher.kt │ │ │ ├── overlay │ │ │ └── OverlayCreator.kt │ │ │ └── search │ │ │ └── Search.kt │ └── res │ │ ├── drawable-hdpi │ │ └── ic_notification_icon.png │ │ ├── drawable-mdpi │ │ └── ic_notification_icon.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_notification_icon.png │ │ ├── drawable-xxhdpi │ │ └── ic_notification_icon.png │ │ ├── drawable-xxxhdpi │ │ └── ic_notification_icon.png │ │ ├── drawable │ │ ├── ic_empty_user.png │ │ ├── ic_empty_user_background.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_spam.png │ │ ├── ic_spam_background.xml │ │ ├── ic_spam_image_background.xml │ │ └── ic_spam_round.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_overlay.xml │ │ ├── activity_statistics.xml │ │ ├── content_main.xml │ │ ├── element_log.xml │ │ └── settings_activity.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ └── darkglamour.js │ │ ├── values-night │ │ └── colors.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── root_preferences.xml │ └── test │ └── java │ └── com │ └── leti │ └── phonedetector │ ├── DBTest.kt │ └── ExampleUnitTest.kt ├── build.gradle ├── docs ├── UI.pdf ├── UI_big.png ├── UI_small.png ├── UseCases.md ├── UseCases │ ├── analog │ │ ├── ad.png │ │ ├── contact_from_logs │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ │ ├── identification │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ ├── notification │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ │ ├── settings │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── 8.png │ │ │ └── 9.png │ │ └── spam_from_logs │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ └── 5.png │ ├── contact_from_logs │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── identification │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── notification │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ ├── possible_improve │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── settings │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ └── spam_from_logs │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png ├── db │ ├── db.drawio │ └── db.png ├── Пояснительная_записка.docx └── Пояснительная_записка.pdf ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── play_market_publication ├── icon.png ├── screenshot_1.png ├── screenshot_2.png └── screenshot_3.png └── settings.gradle /.github/workflows/android-test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: 10 | - '*' 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "build" 15 | build: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-18.04 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v1 23 | 24 | - name: set up JDK 1.8 25 | uses: actions/setup-java@v1 26 | with: 27 | java-version: 1.8 28 | 29 | - name: Unit tests 30 | run: | 31 | bash ./gradlew test --stacktrace 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Android Studio 88 | .idea/ 89 | src/.idea/* 90 | app/release/output.json 91 | app/release/output-metadata.json 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | dist : trusty 4 | 5 | env: 6 | global: 7 | - ANDROID_API_LEVEL=28 8 | - ANDROID_BUILD_TOOLS_VERSION=28.0.3 9 | - ANDROID_ABI=armeabi-v7a 10 | 11 | android: 12 | components: 13 | - tools 14 | - platform-tools 15 | - extra-android-m2repository 16 | licenses: 17 | - 'android-sdk-preview-license-52d11cd2' 18 | - 'android-sdk-license-.+' 19 | - 'google-gdk-license-.+' 20 | 21 | before_install: 22 | - touch $HOME/.android/repositories.cfg 23 | - yes | sdkmanager "platforms;android-28" 24 | - yes | sdkmanager "build-tools;28.0.3" 25 | 26 | before_cache: 27 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 28 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 29 | 30 | cache: 31 | directories: 32 | - $HOME/.gradle/caches/ 33 | - $HOME/.gradle/wrapper/ 34 | - $HOME/.android/build-cache 35 | 36 | before_script: 37 | - chmod +x gradlew 38 | 39 | script: 40 | - ./gradlew clean assembleDebug assembleRelease ktlint ktlintFormat 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kovynev Maxim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phone Detector 2 | 3 | [![Build Status](https://travis-ci.com/kovinevmv/PhoneDetector.svg?branch=master)](https://travis-ci.com/kovinevmv/PhoneDetector) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/67bfad1bd6a843c5847c16aba9228ccb)](https://app.codacy.com/manual/kovinevmv/PhoneDetector?utm_source=github.com&utm_medium=referral&utm_content=kovinevmv/PhoneDetector&utm_campaign=Badge_Grade_Dashboard) 5 | [![time tracker](https://wakatime.com/badge/github/kovinevmv/PhoneDetector.svg)](https://wakatime.com/badge/github/kovinevmv/PhoneDetector) 6 | 7 | ## App screens 8 | 9 | | | | | 10 | :---:|:---:|:---: 11 | ![](play_market_publication/screenshot_1.png) | ![](play_market_publication/screenshot_2.png) | ![](play_market_publication/screenshot_3.png) 12 | 13 | 14 | ### Donate for coffee 15 | 16 | | Boosty | 17 | | ------------- | 18 | | | 19 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | indent_size=4 3 | insert_final_newline=true 4 | max_line_length=off 5 | disabled_rules=no-wildcard-imports,indent 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 29 9 | defaultConfig { 10 | applicationId "com.leti.phonedetector" 11 | minSdkVersion 23 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.4.2" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | testOptions { 24 | animationsDisabled = false 25 | unitTests { 26 | includeAndroidResources = true 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | implementation 'androidx.appcompat:appcompat:1.2.0' 35 | implementation 'androidx.core:core-ktx:1.3.1' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | implementation 'com.google.android.material:material:1.2.0' 38 | implementation 'androidx.preference:preference:1.1.1' 39 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" 40 | implementation("org.jsoup:jsoup:1.11.3") 41 | implementation('com.googlecode.libphonenumber:libphonenumber:8.11.5') 42 | implementation('com.github.kittinunf.fuel:fuel:2.2.1') 43 | implementation('com.github.AnyChart:AnyChart-Android:1.1.2') 44 | testImplementation 'androidx.test:core:1.2.0' 45 | testImplementation 'junit:junit:4.12' 46 | testImplementation 'org.robolectric:robolectric:4.3' 47 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 49 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' 50 | androidTestImplementation 'androidx.test:runner:1.2.0' 51 | androidTestImplementation 'androidx.test:rules:1.2.0' 52 | testImplementation 'org.hamcrest:hamcrest:2.2' 53 | androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' 54 | implementation 'com.github.tsuryo:Swipeable-RecyclerView:1.1' 55 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 56 | } 57 | 58 | apply from: "ktlint.gradle" 59 | -------------------------------------------------------------------------------- /app/ktlint.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | jcenter() 3 | } 4 | 5 | configurations { 6 | ktlint 7 | } 8 | 9 | dependencies { 10 | ktlint "com.pinterest:ktlint:0.36.0" 11 | } 12 | 13 | task ktlint(type: JavaExec, group: "verification") { 14 | description = "Check Kotlin code style." 15 | classpath = configurations.ktlint 16 | main = "com.pinterest.ktlint.Main" 17 | args "src/**/*.kt" 18 | } 19 | 20 | check.dependsOn ktlint 21 | 22 | task ktlintFormat(type: JavaExec, group: "formatting") { 23 | description = "Fix Kotlin code style deviations." 24 | classpath = configurations.ktlint 25 | main = "com.pinterest.ktlint.Main" 26 | args "-F", "src/**/*.kt" 27 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/leti/phonedetector/UITest.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.content.Context 4 | import android.view.KeyEvent 5 | import android.view.View 6 | import android.widget.EditText 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.test.core.app.ApplicationProvider 10 | import androidx.test.espresso.Espresso.* 11 | import androidx.test.espresso.action.ViewActions 12 | import androidx.test.espresso.action.ViewActions.* 13 | import androidx.test.espresso.assertion.ViewAssertions.doesNotExist 14 | import androidx.test.espresso.assertion.ViewAssertions.matches 15 | import androidx.test.espresso.contrib.RecyclerViewActions 16 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem 17 | import androidx.test.espresso.matcher.ViewMatchers.* 18 | import androidx.test.ext.junit.runners.AndroidJUnit4 19 | import androidx.test.filters.LargeTest 20 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 21 | import androidx.test.rule.ActivityTestRule 22 | import com.leti.phonedetector.database.PhoneLogDBHelper 23 | import org.hamcrest.CoreMatchers.allOf 24 | import org.hamcrest.Description 25 | import org.hamcrest.Matcher 26 | import org.hamcrest.TypeSafeMatcher 27 | import org.junit.Before 28 | import org.junit.Rule 29 | import org.junit.Test 30 | import org.junit.runner.RunWith 31 | 32 | @RunWith(AndroidJUnit4::class) 33 | @LargeTest 34 | class UITest { 35 | 36 | @Before 37 | fun fillDb() { 38 | val context = ApplicationProvider.getApplicationContext() 39 | val db = PhoneLogDBHelper(context) 40 | db.fillSampleData() 41 | } 42 | 43 | @get:Rule 44 | val activityRule = ActivityTestRule(MainActivity::class.java) 45 | 46 | @Test 47 | fun filterLog() { 48 | openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) 49 | onView(withText("Show spam")).perform(click()) 50 | openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) 51 | onView(withText("Show not spam")).perform(click()) 52 | onView(withId(R.id.log_layout)).check(doesNotExist()) 53 | openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) 54 | onView(withText("Show spam")).perform(click()) 55 | openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) 56 | onView(withText("Show not spam")).perform(click()) 57 | onView(allOf(withId(R.id.log_layout), isDisplayed())) 58 | } 59 | 60 | @Test 61 | fun testSample() { 62 | if (getRVcount() > 0) { 63 | onView(withId(R.id.list_of_phones)) 64 | .perform( 65 | RecyclerViewActions.actionOnItemAtPosition( 66 | 0, 67 | click() 68 | ) 69 | ) 70 | onView(withId(R.id.overlay_layout)).check(matches(isDisplayed())) 71 | onView(withText("Block number")).check(matches(isDisplayed())) 72 | onView(withId(R.id.overlay_button_exit)).perform(click()) 73 | onView(withId(R.id.overlay_layout)).check(doesNotExist()) 74 | onView(withId(R.id.list_of_phones)) 75 | .perform( 76 | RecyclerViewActions.actionOnItemAtPosition( 77 | 1, 78 | click() 79 | ) 80 | ) 81 | onView(withId(R.id.overlay_layout)).check(matches(isDisplayed())) 82 | onView(withText(R.string.button_add_contact)).check(matches(isDisplayed())) 83 | onView(withId(R.id.overlay_button_exit)).check(matches(isDisplayed())) 84 | } 85 | } 86 | 87 | @Test 88 | fun testSearch() { 89 | onView(withId(R.id.action_search)).perform(click()) 90 | onView(withId(R.id.search_src_text)).perform(typeText("Max")) 91 | onView(withId(R.id.log_element_text_name)).check(matches(hasValueEqualTo("Max") as Matcher?)) 92 | onView(withId(R.id.list_of_phones)) 93 | .perform( 94 | RecyclerViewActions.actionOnItemAtPosition( 95 | 0, 96 | click() 97 | ) 98 | ) 99 | onView(withId(R.id.overlay_layout)).check(matches(isDisplayed())) 100 | onView(withText(R.string.button_add_contact)).check(matches(isDisplayed())) 101 | onView(withId(R.id.overlay_button_exit)).check(matches(isDisplayed())) 102 | onView(withId(R.id.overlay_button_exit)).perform(click()) 103 | } 104 | 105 | @Test 106 | fun testSearchNotFound() { 107 | onView(withId(R.id.action_search)).perform(click()) 108 | onView(withId(R.id.search_src_text)).perform(typeText("+79291045342")) 109 | onView(withId(R.id.search_src_text)).perform(pressKey(KeyEvent.KEYCODE_ENTER)) 110 | onView(withText("No")).perform(click()) 111 | ViewActions.pressBack() 112 | } 113 | 114 | @Test 115 | fun testSettings() { 116 | openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) 117 | onView(withText("Settings")).perform(click()) 118 | onView(withId(androidx.preference.R.id.recycler_view)) 119 | .perform(actionOnItem( 120 | hasDescendant(withText(R.string.use_getcontact)), click())) 121 | onView(withId(androidx.preference.R.id.recycler_view)) 122 | .perform(actionOnItem( 123 | hasDescendant(withText(R.string.use_neberitrubku)), click())) 124 | onView(withId(androidx.preference.R.id.recycler_view)) 125 | .perform(actionOnItem( 126 | hasDescendant(withText(R.string.show_empty_user)), click())) 127 | onView(withId(androidx.preference.R.id.recycler_view)) 128 | .perform(actionOnItem( 129 | hasDescendant(withText(R.string.create_notification)), click())) 130 | onView(withId(androidx.preference.R.id.recycler_view)) 131 | .perform(actionOnItem( 132 | hasDescendant(withText(R.string.notification_instead_overlay)), click())) 133 | onView(withId(androidx.preference.R.id.recycler_view)) 134 | .perform(actionOnItem( 135 | hasDescendant(withText(R.string.no_cache_empty_phones)), click())) 136 | onView(withId(androidx.preference.R.id.recycler_view)) 137 | .perform(actionOnItem( 138 | hasDescendant(withText(R.string.always_network)), click())) 139 | ViewActions.pressBack() 140 | } 141 | 142 | private fun getRVcount(): Int { 143 | val recyclerView = 144 | activityRule.activity.findViewById(R.id.list_of_phones) as RecyclerView 145 | return recyclerView.adapter!!.itemCount 146 | } 147 | } 148 | 149 | fun hasValueEqualTo(content: String): Any { 150 | return object : TypeSafeMatcher() { 151 | override fun describeTo(description: Description) { 152 | description.appendText("Has EditText/TextView the value: $content") 153 | } 154 | 155 | override fun matchesSafely(view: View?): Boolean { 156 | if (view !is TextView && view !is EditText) { 157 | return false 158 | } 159 | val text: String = if (view is TextView) { 160 | view.text.toString() 161 | } else { 162 | (view as EditText).text.toString() 163 | } 164 | return text.equals(content, ignoreCase = true) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 24 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/ic_empty_user-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/ic_empty_user-web.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/ic_spam-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/ic_spam-web.png -------------------------------------------------------------------------------- /app/src/main/ic_spam_image-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/ic_spam_image-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/DataAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.content.* 4 | import android.graphics.BitmapFactory 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.view.animation.Animation 9 | import android.view.animation.AnimationUtils 10 | import android.widget.ImageView 11 | import android.widget.LinearLayout 12 | import android.widget.TextView 13 | import android.widget.Toast 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.recyclerview.widget.RecyclerView 16 | import com.leti.phonedetector.model.DEFAULT_IMAGE 17 | import com.leti.phonedetector.model.PhoneLogInfo 18 | 19 | internal class DataAdapter(val context: Context, private var phones: ArrayList) : 20 | 21 | RecyclerView.Adapter() { 22 | 23 | val APP_PREFERENCES = "PHONEDETECTOR_PREFERENCES" 24 | private var sharedPreferences: SharedPreferences 25 | private val inflater: LayoutInflater = LayoutInflater.from(context) 26 | 27 | private var lastPosition = -1 28 | 29 | init { 30 | sharedPreferences = context.getSharedPreferences(APP_PREFERENCES, AppCompatActivity.MODE_PRIVATE) 31 | this.update(phones) 32 | } 33 | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 35 | val view = inflater.inflate(R.layout.element_log, parent, false) 36 | return ViewHolder(view) 37 | } 38 | 39 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 40 | val phone = phones[position] 41 | when (phone.isSpam) { 42 | true -> holder.imageView.setImageResource(R.drawable.ic_spam) 43 | false -> holder.imageView.setImageResource(R.drawable.ic_empty_user) 44 | } 45 | holder.nameView.text = if (phone.name.length < 25) phone.name else phone.name.take(25) + "..." 46 | holder.numberView.text = phone.number 47 | holder.timeView.text = phone.time.take(2 + 1 + 2) 48 | holder.dateView.text = phone.date 49 | if (phone.image != DEFAULT_IMAGE) holder.imageView.setImageBitmap(BitmapFactory.decodeFile(phone.image)) 50 | 51 | holder.initClick(phone) 52 | setAnimation(holder.itemView, position) 53 | } 54 | 55 | private fun setAnimation(viewToAnimate: View, position: Int) { 56 | if (position > lastPosition) { 57 | val animation: Animation = 58 | AnimationUtils.loadAnimation(context, android.R.anim.fade_in) 59 | viewToAnimate.startAnimation(animation) 60 | lastPosition = position 61 | } 62 | } 63 | 64 | override fun getItemCount(): Int { 65 | return phones.size 66 | } 67 | 68 | fun update(data: ArrayList) { 69 | phones = filterShow(data) 70 | this.notifyDataSetChanged() 71 | } 72 | 73 | private fun filterShow(data: ArrayList): ArrayList { 74 | val showSpam: Boolean = sharedPreferences.getBoolean("is_show_spam", true) 75 | val showNotSpam: Boolean = sharedPreferences.getBoolean("is_show_not_spam", true) 76 | 77 | return when { 78 | showSpam && !showNotSpam -> ArrayList(data.filter { it.isSpam }) 79 | !showSpam && showNotSpam -> ArrayList(data.filter { !it.isSpam }) 80 | showSpam && showNotSpam -> data 81 | !showSpam && !showNotSpam -> ArrayList() 82 | else -> data 83 | } 84 | } 85 | 86 | inner class ViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) { 87 | internal val imageView: ImageView = view.findViewById(R.id.log_element_user_image) as ImageView 88 | internal val nameView: TextView = view.findViewById(R.id.log_element_text_name) as TextView 89 | internal val numberView: TextView = view.findViewById(R.id.log_element_text_number) as TextView 90 | private val logLayout: LinearLayout = view.findViewById(R.id.log_layout) as LinearLayout 91 | internal val timeView: TextView = view.findViewById(R.id.log_element_text_time) as TextView 92 | internal val dateView: TextView = view.findViewById(R.id.log_element_text_date) as TextView 93 | 94 | fun initClick(phone: PhoneLogInfo) { 95 | logLayout.setOnClickListener { 96 | val mIntent = Intent(this@DataAdapter.context, OverlayActivity::class.java) 97 | mIntent.putExtra("user", phone.toPhoneInfo()) 98 | mIntent.putExtra("is_display_buttons", true) 99 | this@DataAdapter.context.startActivity(mIntent) 100 | } 101 | logLayout.setOnLongClickListener { 102 | Toast.makeText(this@DataAdapter.context, "Number has been copied to clipboard", Toast.LENGTH_SHORT).show() 103 | 104 | val number = numberView.text 105 | val clipboard = this@DataAdapter.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 106 | val clip = ClipData.newPlainText("ADD_PHONE_NUMBER_$number", number) 107 | clipboard.setPrimaryClip(clip) 108 | return@setOnLongClickListener true 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/OverlayActivity.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.BitmapFactory 9 | import android.os.Build 10 | import android.os.Bundle 11 | import android.provider.ContactsContract 12 | import android.telecom.TelecomManager 13 | import android.view.View 14 | import android.widget.Toast 15 | import androidx.annotation.RequiresApi 16 | import androidx.appcompat.app.AppCompatActivity 17 | import com.leti.phonedetector.bitmap.BitmapReader 18 | import com.leti.phonedetector.model.DEFAULT_IMAGE 19 | import com.leti.phonedetector.model.PhoneInfo 20 | import kotlinx.android.synthetic.main.activity_overlay.* 21 | 22 | class OverlayActivity : AppCompatActivity() { 23 | 24 | private lateinit var user: PhoneInfo 25 | 26 | @RequiresApi(Build.VERSION_CODES.N) 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | setContentView(R.layout.activity_overlay) 30 | 31 | createUserByIntentExtra() 32 | } 33 | 34 | @RequiresApi(Build.VERSION_CODES.N) 35 | private fun createUserByIntentExtra() { 36 | user = intent.getParcelableExtra("user") ?: return 37 | 38 | overlay_text_view_number.text = user.number 39 | overlay_text_view_name.text = user.name 40 | overlay_tags.text = user.tags.joinToString(separator = "\n") 41 | 42 | when (user.isSpam) { 43 | true -> setSpamSettings() 44 | false -> setNotSpamSettings() 45 | } 46 | 47 | if (user.image != DEFAULT_IMAGE) overlay_user_image.setImageBitmap(BitmapFactory.decodeFile(user.image)) 48 | overlay_button_exit.setOnClickListener { finish() } 49 | } 50 | 51 | @SuppressLint("ServiceCast", "Recycle") 52 | @RequiresApi(Build.VERSION_CODES.N) 53 | private fun setSpamSettings() { 54 | overlay_user_image.setImageResource(R.drawable.ic_spam) 55 | val isDisplayButtons = intent.getBooleanExtra("is_display_buttons", true) 56 | if (!isDisplayButtons) { 57 | disableActionButton() 58 | } else { 59 | overlay_button_action.text = resources.getString(R.string.button_block_number) 60 | 61 | overlay_button_action.setOnClickListener { 62 | Toast.makeText(this@OverlayActivity, "Number has been copied to clipboard", Toast.LENGTH_SHORT).show() 63 | 64 | val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 65 | val clip = ClipData.newPlainText("BLOCKED_NUMBER_${user.number}", user.number) 66 | clipboard.setPrimaryClip(clip) 67 | 68 | val telecomManager = this.getSystemService(Context.TELECOM_SERVICE) as TelecomManager 69 | this.startActivity(telecomManager.createManageBlockedNumbersIntent(), null) 70 | } 71 | } 72 | } 73 | 74 | private fun setNotSpamSettings() { 75 | overlay_user_image.setImageResource(R.drawable.ic_empty_user) 76 | val isDisplayButtons = intent.getBooleanExtra("is_display_buttons", true) 77 | if (!isDisplayButtons) { 78 | disableActionButton() 79 | } else { 80 | 81 | overlay_button_action.text = resources.getString(R.string.button_add_contact) 82 | 83 | overlay_button_action.setOnClickListener { 84 | val contactIntent = Intent(ContactsContract.Intents.Insert.ACTION) 85 | contactIntent.type = ContactsContract.RawContacts.CONTENT_TYPE 86 | 87 | if (user.image != DEFAULT_IMAGE) { 88 | val data = BitmapReader().readFile(user.image) 89 | contactIntent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, data) 90 | } 91 | 92 | contactIntent 93 | .putExtra(ContactsContract.Intents.Insert.NAME, user.name) 94 | .putExtra(ContactsContract.Intents.Insert.PHONE, user.number) 95 | 96 | startActivityForResult(contactIntent, 1) 97 | } 98 | } 99 | } 100 | 101 | private fun disableActionButton() { 102 | overlay_button_action.visibility = View.GONE 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/PhoneStateReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Handler 8 | import android.telephony.TelephonyManager 9 | import android.telephony.TelephonyManager.ACTION_PHONE_STATE_CHANGED 10 | import androidx.preference.PreferenceManager 11 | import com.leti.phonedetector.contacts.Contacts 12 | import com.leti.phonedetector.model.PhoneLogInfo 13 | import com.leti.phonedetector.notification.BlockNotification 14 | import com.leti.phonedetector.notification.IncomingNotification 15 | import com.leti.phonedetector.overlay.OverlayCreator 16 | import com.leti.phonedetector.search.Search 17 | import java.util.* 18 | 19 | class PhoneStateReceiver : BroadcastReceiver() { 20 | 21 | @SuppressLint("SimpleDateFormat") 22 | override fun onReceive(context: Context, intent: Intent) { 23 | if (intent.action == ACTION_PHONE_STATE_CHANGED) { 24 | 25 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) 26 | val isRun = sharedPreferences.getBoolean("activate_phone_detection_switch", false) 27 | val notFindInContacts = sharedPreferences.getBoolean("disable_search_in_contacts_switch", false) 28 | val showEmptyUser = sharedPreferences.getBoolean("show_empty_user", false) 29 | val isCreatePushUp = sharedPreferences.getBoolean("notification_switch", false) 30 | val delayNotificationTime = sharedPreferences.getInt("time_notification", 1) 31 | val isShowNotificationInsteadOfPopup = sharedPreferences.getBoolean("notification_instead_overlay", false) 32 | 33 | if (!isRun) return 34 | 35 | val state = intent.getStringExtra(TelephonyManager.EXTRA_STATE) 36 | val incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) 37 | 38 | when (state) { 39 | TelephonyManager.EXTRA_STATE_RINGING -> { 40 | Handler().postDelayed({ 41 | if (incomingNumber != null) { 42 | val searcher = Search(context) 43 | val formattedIncoming = searcher.formatE164NumberRU(incomingNumber) 44 | if (notFindInContacts) { 45 | val contactName = Contacts(context).getContactNameByPhone(formattedIncoming) 46 | if (contactName != null) return@postDelayed 47 | } 48 | 49 | val user = searcher.startPhoneDetection(formattedIncoming) 50 | if (!user.toPhoneInfo().isDefault() || showEmptyUser) { 51 | val overlayCreator = OverlayCreator(context) 52 | 53 | if (isShowNotificationInsteadOfPopup) { 54 | val mIntent = overlayCreator.createIntent(user.toPhoneInfo(), true) 55 | IncomingNotification(context, mIntent, user.toPhoneInfo()).notifyNow() 56 | } else { 57 | val mIntentEnabledButtons = overlayCreator.createIntent(user.toPhoneInfo(), false) 58 | context.startActivity(mIntentEnabledButtons) 59 | } 60 | } 61 | } 62 | }, 100) 63 | } 64 | TelephonyManager.EXTRA_STATE_OFFHOOK -> {} 65 | TelephonyManager.EXTRA_STATE_IDLE -> { 66 | if (incomingNumber != null && isCreatePushUp) { 67 | val searcher = Search(context) 68 | val formattedIncoming = searcher.formatE164NumberRU(incomingNumber) 69 | val user = searcher.findUserByPhone(formattedIncoming) 70 | val overlayCreator = OverlayCreator(context) 71 | val intentOnPushUpClick = overlayCreator.createIntent(user, true) 72 | if (user.isSpam) 73 | BlockNotification(context, intentOnPushUpClick, user).notify(delayNotificationTime) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | private fun showNotification(context: Context, phone: PhoneLogInfo) { 81 | val overlayCreator = OverlayCreator(context) 82 | val intent = overlayCreator.createIntent(phone.toPhoneInfo(), true) 83 | BlockNotification(context, intent, phone.toPhoneInfo()).createNotification() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.content.Intent 4 | import android.content.pm.PackageManager 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.provider.Settings 8 | import android.widget.Toast 9 | import androidx.appcompat.app.AlertDialog 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.appcompat.app.AppCompatDelegate 12 | import androidx.core.content.ContextCompat.checkSelfPermission 13 | import androidx.preference.Preference 14 | import androidx.preference.PreferenceFragmentCompat 15 | import androidx.preference.SwitchPreferenceCompat 16 | import com.leti.phonedetector.database.PhoneLogDBHelper 17 | import com.leti.phonedetector.database.TokenDBHelper 18 | 19 | class SettingsActivity : AppCompatActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.settings_activity) 24 | supportFragmentManager 25 | .beginTransaction() 26 | .replace(R.id.settings, SettingsFragment()) 27 | .commit() 28 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 29 | } 30 | 31 | override fun onSupportNavigateUp(): Boolean { 32 | onBackPressed() 33 | return true 34 | } 35 | 36 | class SettingsFragment : PreferenceFragmentCompat() { 37 | private val REQUEST_CODE_READ_CALL_LOG = 1 38 | private val REQUEST_CODE_READ_OVERLAY = 2 39 | private val REQUEST_CODE_CONTACTS = 3 40 | private val REQUEST_CODE_CALL = 4 41 | 42 | private lateinit var activatePhoneDetectionSwitch: SwitchPreferenceCompat 43 | private lateinit var disableSearchInContactsSwitch: SwitchPreferenceCompat 44 | private lateinit var makeCallOnSwipeSwitch: SwitchPreferenceCompat 45 | private lateinit var dropTables: Preference 46 | private lateinit var changeThemeSwitch: SwitchPreferenceCompat 47 | 48 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 49 | setPreferencesFromResource(R.xml.root_preferences, rootKey) 50 | 51 | activatePhoneDetectionSwitch = preferenceScreen.findPreference("activate_phone_detection_switch")!! 52 | disableSearchInContactsSwitch = preferenceScreen.findPreference("disable_search_in_contacts_switch")!! 53 | makeCallOnSwipeSwitch = preferenceScreen.findPreference("make_call_on_swipe")!! 54 | dropTables = preferenceScreen.findPreference("drop_table")!! 55 | changeThemeSwitch = preferenceScreen.findPreference("dark_mode")!! 56 | 57 | dropTables.setOnPreferenceClickListener { 58 | val builder = AlertDialog.Builder(requireContext()) 59 | builder.setTitle("Clean all log and phone information") 60 | builder.setMessage("This action cannot be undone. Information from logs and all detected numbers will be deleted") 61 | 62 | builder.setPositiveButton(android.R.string.yes) { _, _ -> 63 | val db = PhoneLogDBHelper(requireContext()) 64 | db.cleanTables() 65 | Toast.makeText(context, 66 | "Database was cleaned", Toast.LENGTH_SHORT).show() 67 | } 68 | 69 | builder.setNegativeButton(android.R.string.no) { _, _ -> } 70 | builder.show() 71 | 72 | val db = TokenDBHelper(requireContext()) 73 | val tokens = db.getTokens() 74 | 75 | val toastText = mutableListOf("Tokens info:") 76 | for (token in tokens) { 77 | toastText.add("Token: ${token.token.take(10)}..., Count: ${token.remainCount}") 78 | } 79 | Toast.makeText(requireContext(), toastText.joinToString("\n"), Toast.LENGTH_LONG).show() 80 | 81 | return@setOnPreferenceClickListener true 82 | } 83 | 84 | activatePhoneDetectionSwitch.setOnPreferenceClickListener { 85 | if (activatePhoneDetectionSwitch.isChecked) { 86 | callLogRequestPermissions() 87 | } 88 | return@setOnPreferenceClickListener true 89 | } 90 | disableSearchInContactsSwitch.setOnPreferenceClickListener { 91 | if (disableSearchInContactsSwitch.isChecked) { 92 | callContactPermission() 93 | } 94 | return@setOnPreferenceClickListener true 95 | } 96 | 97 | makeCallOnSwipeSwitch.setOnPreferenceClickListener { 98 | if (makeCallOnSwipeSwitch.isChecked) { 99 | callCallPermission() 100 | } 101 | return@setOnPreferenceClickListener true 102 | } 103 | 104 | changeThemeSwitch.setOnPreferenceClickListener { 105 | if (changeThemeSwitch.isChecked) { 106 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 107 | } else { 108 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 109 | } 110 | Toast.makeText(this@SettingsFragment.requireContext(), "Application may need to be restarted to apply the theme", Toast.LENGTH_LONG).show() 111 | 112 | return@setOnPreferenceClickListener true 113 | } 114 | } 115 | 116 | private fun checkCallLogPermissions(): Array { 117 | val arrayList = ArrayList() 118 | 119 | when (context?.let { checkSelfPermission(it, android.Manifest.permission.READ_CALL_LOG) }) { 120 | PackageManager.PERMISSION_DENIED -> arrayList.add(android.Manifest.permission.READ_CALL_LOG) 121 | } 122 | when (context?.let { checkSelfPermission(it, android.Manifest.permission.READ_PHONE_STATE) }) { 123 | PackageManager.PERMISSION_DENIED -> arrayList.add(android.Manifest.permission.READ_PHONE_STATE) 124 | } 125 | return arrayList.toTypedArray() 126 | } 127 | 128 | private fun checkCallPermissions(): Array { 129 | val arrayList = ArrayList() 130 | 131 | when (context?.let { checkSelfPermission(it, android.Manifest.permission.CALL_PHONE) }) { 132 | PackageManager.PERMISSION_DENIED -> arrayList.add(android.Manifest.permission.CALL_PHONE) 133 | } 134 | return arrayList.toTypedArray() 135 | } 136 | 137 | private fun checkContactPermissions(): Array { 138 | val arrayList = ArrayList() 139 | 140 | when (context?.let { checkSelfPermission(it, android.Manifest.permission.READ_CONTACTS) }) { 141 | PackageManager.PERMISSION_DENIED -> arrayList.add(android.Manifest.permission.READ_CONTACTS) 142 | } 143 | return arrayList.toTypedArray() 144 | } 145 | 146 | private fun callCallPermission() { 147 | val arrayList = checkCallPermissions() 148 | if (arrayList.isNotEmpty()) { 149 | requestPermissions(arrayList, REQUEST_CODE_CALL) 150 | } 151 | } 152 | 153 | private fun callContactPermission() { 154 | val arrayList = checkContactPermissions() 155 | if (arrayList.isNotEmpty()) { 156 | requestPermissions(arrayList, REQUEST_CODE_CONTACTS) 157 | } 158 | } 159 | 160 | private fun callLogRequestPermissions() { 161 | val arrayList = checkCallLogPermissions() 162 | if (arrayList.isNotEmpty()) { 163 | requestPermissions(arrayList, REQUEST_CODE_READ_CALL_LOG) 164 | } 165 | 166 | if (!Settings.canDrawOverlays(context)) { 167 | val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 168 | Uri.parse("package:${context?.packageName}")) 169 | startActivityForResult(intent, REQUEST_CODE_READ_OVERLAY) 170 | } 171 | } 172 | 173 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 174 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 175 | if (requestCode == REQUEST_CODE_READ_CALL_LOG) { 176 | val arrayList = checkCallLogPermissions() 177 | if (arrayList.isNotEmpty()) { 178 | Toast.makeText(context, "Not all permissions granted. Can't enable phone detection", Toast.LENGTH_SHORT).show() 179 | activatePhoneDetectionSwitch.isChecked = false 180 | } 181 | } 182 | 183 | if (requestCode == REQUEST_CODE_CONTACTS) { 184 | val arrayList = checkContactPermissions() 185 | if (arrayList.isNotEmpty()) { 186 | Toast.makeText(context, "Not permission granted. Search all phones", Toast.LENGTH_SHORT).show() 187 | disableSearchInContactsSwitch.isChecked = false 188 | } 189 | } 190 | 191 | if (requestCode == REQUEST_CODE_CALL) { 192 | val arrayList = checkCallPermissions() 193 | if (arrayList.isNotEmpty()) { 194 | Toast.makeText(context, "Not permission granted. Can't call :(", Toast.LENGTH_SHORT).show() 195 | makeCallOnSwipeSwitch.isChecked = false 196 | } 197 | } 198 | } 199 | 200 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 201 | if (requestCode == REQUEST_CODE_READ_OVERLAY) { 202 | if (!Settings.canDrawOverlays(context)) { 203 | Toast.makeText(context, "Overlay not granted", Toast.LENGTH_SHORT).show() 204 | activatePhoneDetectionSwitch.isChecked = false 205 | } 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/StatisticsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.RawRes 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.preference.PreferenceManager 7 | import com.anychart.APIlib 8 | import com.anychart.AnyChart 9 | import com.anychart.AnyChartView 10 | import com.anychart.chart.common.dataentry.CategoryValueDataEntry 11 | import com.anychart.chart.common.dataentry.DataEntry 12 | import com.anychart.chart.common.dataentry.ValueDataEntry 13 | import com.anychart.chart.common.listener.Event 14 | import com.anychart.chart.common.listener.ListenersInterface 15 | import com.anychart.enums.* 16 | import com.anychart.scales.OrdinalColor 17 | import com.leti.phonedetector.database.PhoneLogDBHelper 18 | import com.leti.phonedetector.model.PhoneLogInfo 19 | import java.util.* 20 | import kotlinx.android.synthetic.main.activity_statistics.* 21 | 22 | class StatisticsActivity : AppCompatActivity() { 23 | 24 | private lateinit var phones: ArrayList 25 | private var isDarkMode: Boolean = false 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | setContentView(R.layout.activity_statistics) 30 | 31 | title = "Statistics" 32 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 33 | 34 | val sharedPreferencesGlobal = PreferenceManager.getDefaultSharedPreferences(this) 35 | isDarkMode = sharedPreferencesGlobal.getBoolean("dark_mode", false) 36 | 37 | readDataBase() 38 | 39 | createSpamChart() 40 | createPieChart() 41 | createWordCloud() 42 | } 43 | 44 | private fun readDataBase() { 45 | val db = PhoneLogDBHelper(this) 46 | phones = db.readPhoneLog() 47 | } 48 | 49 | override fun onSupportNavigateUp(): Boolean { 50 | onBackPressed() 51 | return true 52 | } 53 | 54 | fun getResourceAsText(@RawRes id: Int): String = resources.openRawResource(id).bufferedReader().use { it.readText() } 55 | 56 | private fun dateToMapKey(month: Int, year: Int): String { 57 | val monthString = month.toString().padStart(2, '0') 58 | val yearString = year.toString() 59 | 60 | return "$yearString.$monthString" 61 | } 62 | 63 | private fun createSpamChart() { 64 | val spamView: AnyChartView = findViewById(R.id.chart_spam) 65 | APIlib.getInstance().setActiveAnyChartView(chart_spam) 66 | spamView.setProgressBar(findViewById(R.id.progress_bar_chart_spam)) 67 | 68 | if (this.isDarkMode) { 69 | APIlib.getInstance().addJSLine(getResourceAsText(R.raw.darkglamour).trimIndent()) 70 | APIlib.getInstance().addJSLine("anychart.theme(anychart.themes.darkGlamour);") 71 | } 72 | 73 | val spam = AnyChart.column() 74 | 75 | val calendar: Calendar = Calendar.getInstance() 76 | val currentYear: Int = calendar.get(Calendar.YEAR) 77 | val currentMonth: Int = calendar.get(Calendar.MONTH) + 1 78 | 79 | val phonesMap = mutableMapOf() 80 | 81 | if (currentMonth == 12) { 82 | // Full year 83 | for (month in 1..12) { 84 | phonesMap[dateToMapKey(month, currentYear)] = 0 85 | } 86 | } else { 87 | // Past year 88 | for (month in (currentMonth + 1)..12) { 89 | phonesMap[dateToMapKey(month, currentYear - 1)] = 0 90 | } 91 | // Current year 92 | for (month in 1..(currentMonth)) { 93 | phonesMap[dateToMapKey(month, currentYear)] = 0 94 | } 95 | } 96 | 97 | val innerPhones = phones.filter { p -> p.isSpam } 98 | for (phone in innerPhones) { 99 | val date = phone.date.substring(0, 7) 100 | 101 | if (phonesMap.containsKey(date)) 102 | phonesMap[date] = phonesMap.getOrPut(date) { 1 } + 1 103 | } 104 | 105 | val spamData: MutableList = ArrayList() 106 | 107 | for ((k, v) in phonesMap) { 108 | spamData.add(ValueDataEntry(k.substring(5, 7) + '.' + k.substring(2, 4), v)) 109 | } 110 | 111 | var column = spam.column(spamData) 112 | 113 | column.tooltip() 114 | .titleFormat("{%X}") 115 | .position(Position.CENTER_BOTTOM) 116 | .anchor(Anchor.CENTER_BOTTOM) 117 | .offsetX(0.0) 118 | .offsetY(10.0) 119 | .format("{%Value} calls") 120 | 121 | spam.animation(true) 122 | spam.title("Spam call for the last 12 months").enabled(true) 123 | 124 | spam.yScale().minimum(0.0) 125 | spam.yScale().ticks().allowFractional(false) 126 | 127 | spam.yAxis(0).labels().format("{%Value}{groupsSeparator: }") 128 | spam.xAxis(0).labels().format("{%Value}{groupsSeparator: }") 129 | 130 | val xAxisLabels = spam.xAxis(0).labels() 131 | xAxisLabels.rotation(270) 132 | 133 | spam.tooltip().positionMode(TooltipPositionMode.POINT) 134 | spam.interactivity().hoverMode(HoverMode.BY_X) 135 | 136 | spam.xAxis(0).title("Month") 137 | spam.yAxis(0).title("Number of calls") 138 | 139 | spam.yGrid(0).enabled(true) 140 | 141 | spamView.setChart(spam) 142 | } 143 | 144 | private fun createPieChart() { 145 | val chart: AnyChartView = findViewById(R.id.chart_pie_top) 146 | APIlib.getInstance().setActiveAnyChartView(chart_pie_top) 147 | 148 | if (this.isDarkMode) { 149 | APIlib.getInstance().addJSLine(getResourceAsText(R.raw.darkglamour).trimIndent()) 150 | APIlib.getInstance().addJSLine("anychart.theme(anychart.themes.darkGlamour);") 151 | } 152 | 153 | chart.setProgressBar(findViewById(R.id.progress_bar_chart_pie_top)) 154 | 155 | val pie = AnyChart.pie() 156 | 157 | pie.setOnClickListener(object : 158 | ListenersInterface.OnClickListener(arrayOf("x", "value")) { 159 | override fun onClick(event: Event) {} 160 | }) 161 | 162 | val data: MutableList = ArrayList() 163 | data.add( 164 | ValueDataEntry( 165 | "Spam incoming calls", 166 | phones.filter { phone -> phone.isSpam }.size 167 | ) 168 | ) 169 | data.add( 170 | ValueDataEntry( 171 | "Not spam incoming calls", 172 | phones.filter { phone -> !phone.isSpam }.size 173 | ) 174 | ) 175 | 176 | pie.data(data) 177 | pie.title("Incoming Call Pie Chart") 178 | pie.labels().position("outside") 179 | 180 | pie.legend() 181 | .position("center-bottom") 182 | .itemsLayout(LegendLayout.HORIZONTAL) 183 | .align(Align.CENTER) 184 | 185 | chart.setChart(pie) 186 | } 187 | 188 | private fun createWordCloud() { 189 | val anyChartView: AnyChartView = findViewById(R.id.chart_word_cloud) 190 | APIlib.getInstance().setActiveAnyChartView(chart_word_cloud) 191 | 192 | if (this.isDarkMode) { 193 | APIlib.getInstance().addJSLine(getResourceAsText(R.raw.darkglamour).trimIndent()) 194 | APIlib.getInstance().addJSLine("anychart.theme(anychart.themes.darkGlamour);") 195 | } 196 | 197 | anyChartView.setProgressBar(findViewById(R.id.progress_bar_chart_word_cloud)) 198 | 199 | val tagCloud = AnyChart.tagCloud() 200 | 201 | tagCloud.title("Call Frequency Names") 202 | 203 | val ordinalColor = OrdinalColor.instantiate() 204 | ordinalColor.colors( 205 | arrayOf( 206 | "#f14526", "#26959f", "#3b8ad8", "#60727b", "#e24b26" 207 | ) 208 | ) 209 | tagCloud.colorScale(ordinalColor) 210 | tagCloud.angles(arrayOf(-90.0, 0.0, 90.0)) 211 | 212 | tagCloud.colorRange().enabled(true) 213 | tagCloud.colorRange().colorLineSize(15.0) 214 | 215 | val data: MutableList = ArrayList() 216 | 217 | val phonesMap = mutableMapOf>() 218 | phonesMap[true] = mutableMapOf() 219 | phonesMap[false] = mutableMapOf() 220 | 221 | for (phone in phones) { 222 | phonesMap[phone.isSpam]?.set(phone.name, 223 | (phonesMap[phone.isSpam]?.getOrPut(phone.name) { 0 } ?: 0) + 1) 224 | } 225 | 226 | for ((isSpam, v) in phonesMap) { 227 | for ((name, count) in v) { 228 | data.add(CategoryValueDataEntry(name, if (isSpam) "Spam" else "Not spam", count)) 229 | } 230 | } 231 | 232 | tagCloud.data(data) 233 | 234 | anyChartView.setChart(tagCloud) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/GetContact/AESCipher.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api.GetContact 2 | import android.util.Base64 3 | import com.leti.phonedetector.model.Token 4 | import java.nio.charset.Charset 5 | import javax.crypto.Cipher 6 | import javax.crypto.Mac 7 | import javax.crypto.spec.SecretKeySpec 8 | 9 | fun String.unHex(): ByteArray { 10 | return this.chunked(2).map { it.toInt(16).toByte() }.toByteArray() 11 | } 12 | 13 | class AESCipher(var token: Token = Token()) { 14 | private lateinit var cipherDecrypt: Cipher 15 | private lateinit var cipherEncrypt: Cipher 16 | 17 | private val blockSize = 16 18 | 19 | init { 20 | setCiphers() 21 | } 22 | 23 | private fun setCiphers() { 24 | val AES_KEY = SecretKeySpec(token.aesKey.unHex(), "AES") 25 | 26 | cipherDecrypt = Cipher.getInstance("AES/ECB/NOPADDING") 27 | cipherDecrypt.init(Cipher.DECRYPT_MODE, AES_KEY) 28 | 29 | cipherEncrypt = Cipher.getInstance("AES/ECB/NOPADDING") 30 | cipherEncrypt.init(Cipher.ENCRYPT_MODE, AES_KEY) 31 | } 32 | 33 | fun updateToken(token_: Token) { 34 | token = token_ 35 | setCiphers() 36 | } 37 | 38 | fun createSignature(payload: String, timestamp: String): String { 39 | val message = formatMessageToHMAC(payload, timestamp).toByteArray() 40 | val secret = HMAC_KEY.toByteArray() 41 | 42 | val sign = Mac.getInstance("HmacSHA256").run { 43 | init(SecretKeySpec(secret, algorithm)) 44 | doFinal(message) 45 | } 46 | return encodeBase64(sign) 47 | } 48 | 49 | private fun formatMessageToHMAC(msg: String, timestamp: String): String { 50 | return "$timestamp-$msg" 51 | } 52 | 53 | private fun unpadding(s: String): String { 54 | return s.dropLast(s.last().toInt()) 55 | } 56 | 57 | private fun padding(s: String): ByteArray { 58 | val padding = blockSize - s.length % blockSize 59 | return s.toByteArray() + ByteArray(padding) { padding.toByte() } 60 | } 61 | 62 | fun decodeBase64(data: String): ByteArray { 63 | return Base64.decode(data, Base64.DEFAULT) 64 | } 65 | 66 | fun encodeBase64(data: ByteArray): String { 67 | return Base64.encode(data, Base64.DEFAULT).toString(Charset.defaultCharset()) 68 | } 69 | 70 | fun decryptAES(data: String): ByteArray { 71 | return cipherDecrypt.doFinal(data.toByteArray()) 72 | } 73 | 74 | fun decryptAES(data: ByteArray): ByteArray { 75 | return cipherDecrypt.doFinal(data) 76 | } 77 | 78 | fun decryptAESWithBase64(data: String): String { 79 | return unpadding(decryptAES(decodeBase64(data)).toString(Charset.defaultCharset())) 80 | } 81 | 82 | fun encryptAES(data: String): ByteArray { 83 | return cipherEncrypt.doFinal(padding(data)) 84 | } 85 | 86 | fun encryptAESWithBase64(data: String): String { 87 | return encodeBase64(encryptAES(data)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/GetContact/ConfigUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api.GetContact 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.leti.phonedetector.LOG_TAG_VERBOSE 6 | import com.leti.phonedetector.database.TokenDBHelper 7 | import com.leti.phonedetector.model.Token 8 | 9 | class ConfigUpdater(val context: Context) { 10 | private val db = TokenDBHelper(context) 11 | private var tokens: ArrayList 12 | 13 | init { 14 | val tokensInput = arrayOf( 15 | Token(aesKey = "e92c9987a83bb5ad0353769eade2f897cddeb8691bc9a953876478e6caded42b", 16 | androidOS = "android 6.0", deviceId = "3ba530698cff5145", isActive = true, privateKey = 3700313, remainCount = 100, 17 | token = "iiXya3eb642742bd2522ae965f31fdcbdd087293235e0d01a520026c44d", isPrimaryUse = false), 18 | Token(aesKey = "389383a471af66f4e84b6722d59b7d45e771620857e579565763e1fe3e8ebd0a", 19 | androidOS = "android 5.0", deviceId = "14130e29cebe9c39", isActive = true, privateKey = 2047896, remainCount = 100, 20 | token = "hEmffc9d833620e4e13cf96e56d13552c5284f5d99665aa8856a06d4990", isPrimaryUse = true)) 21 | 22 | for (token in tokensInput) { 23 | db.insertToken(token) 24 | } 25 | 26 | tokens = db.getTokens() 27 | } 28 | 29 | fun updateRemainCountByToken(tokenString: String, remainCount: Int) { 30 | val token: Token? = db.findToken(tokenString) 31 | if (token != null) { 32 | token.updateRemainCount(remainCount) 33 | db.updateToken(token) 34 | } 35 | updateStatus() 36 | } 37 | 38 | fun decreaseRemainCountByToken(tokenString: String) { 39 | val token: Token? = db.findToken(tokenString) 40 | if (token != null) { 41 | token.updateRemainCount(token.remainCount - 1) 42 | db.updateToken(token) 43 | } 44 | updateStatus() 45 | } 46 | 47 | private fun updateStatus() { 48 | tokens = db.getTokens() 49 | } 50 | 51 | fun getAllActive(): ArrayList { 52 | return ArrayList(tokens.filter { it.isActive }) 53 | } 54 | 55 | fun getAnyActive(): Token { 56 | val activeTokens = this.getAllActive() 57 | return if (activeTokens.size > 0) activeTokens[0] else Token() 58 | } 59 | 60 | fun getRandomActive(): Token { 61 | val activeTokens = this.getAllActive().shuffled() 62 | return if (activeTokens.isNotEmpty()) activeTokens[0] else Token() 63 | } 64 | 65 | fun getPrimaryUse(): Token { 66 | val primaryTokens = this.getAllActive().filter { it.isPrimaryUse } 67 | Log.d(LOG_TAG_VERBOSE, "Count tokens: ${primaryTokens.size}") 68 | return if (primaryTokens.isNotEmpty()) primaryTokens.first() else this.getRandomActive() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/GetContact/GetContactAPI.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api.GetContact 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.AsyncTask 6 | import android.util.Log 7 | import com.github.kittinunf.fuel.Fuel 8 | import com.leti.phonedetector.LOG_TAG_VERBOSE 9 | import com.leti.phonedetector.model.DEFAULT_IMAGE 10 | import com.leti.phonedetector.model.PhoneInfo 11 | import java.io.File 12 | import org.json.JSONObject 13 | 14 | class GetContactAPI(context: Context, private val timeout: Int) { 15 | private var updater: ConfigUpdater = ConfigUpdater(context) 16 | private var token = updater.getPrimaryUse() 17 | private var requester: Requester = Requester(context, token, timeout) 18 | 19 | fun getAllByPhone(number: String): PhoneInfo { 20 | return callFindAllByPhoneAsync(number) 21 | } 22 | 23 | private fun callFindAllByPhoneAsync(number: String): PhoneInfo { 24 | return NetworkTaskGetContact().execute(number).get() 25 | } 26 | 27 | @SuppressLint("StaticFieldLeak") 28 | inner class NetworkTaskGetContact : AsyncTask() { 29 | override fun doInBackground(vararg parts: String): PhoneInfo { 30 | return try { 31 | val p = this@GetContactAPI.findAllInfoByPhone(parts[0]) 32 | Log.d(LOG_TAG_VERBOSE, p.toString()) 33 | return p 34 | } catch (e: Exception) { 35 | Log.d(LOG_TAG_VERBOSE, "Error on API NetworkTaskGetContact: $e") 36 | PhoneInfo(number = parts[0]) 37 | } 38 | } 39 | } 40 | 41 | private fun parse(s: String?): String? { 42 | return if (s == "null" || s == null || s == "") return null else s 43 | } 44 | 45 | private fun findNameByPhone(number: String): PhoneInfo { 46 | val response = requester.getPhoneName(number) 47 | if (response.isNotBlank() && !JSONObject(response).has("error")) { 48 | val json = JSONObject(response) 49 | val profile = json.getJSONObject("result").getJSONObject("profile") 50 | val name = profile.getString("name") 51 | val surname = profile.getString("surname") 52 | val displayName = profile.getString("displayName") 53 | 54 | val finalName = 55 | if (name == "null" && surname == "null") { 56 | displayName 57 | } else { 58 | "$name $surname" 59 | } 60 | 61 | val country = parse(profile.getString("country")) 62 | 63 | var profileImage = parse(profile.getString("profileImage")) ?: DEFAULT_IMAGE 64 | if (profileImage != DEFAULT_IMAGE) { 65 | profileImage = saveImage(profileImage) 66 | } 67 | 68 | val email = parse(profile.getString("email")) 69 | val isSpam = json.getJSONObject("result").getJSONObject("spamInfo").getString("degree") == "high" 70 | 71 | val tags = ArrayList() 72 | if (country != null) tags.add(country) 73 | if (email != null) tags.add(email) 74 | 75 | val remainCount = json.getJSONObject("result").getJSONObject("subscriptionInfo").getJSONObject("usage").getJSONObject("search").getInt("remainingCount") 76 | updater.updateRemainCountByToken(token.token, remainCount) 77 | token = updater.getPrimaryUse() 78 | requester.updateToken(token) 79 | 80 | return PhoneInfo(number = number, name = finalName, tags = tags.toTypedArray(), isSpam = isSpam, image = profileImage) 81 | } 82 | 83 | return PhoneInfo(number = number) 84 | } 85 | 86 | private fun findTagsByPhone(number: String): Array { 87 | val response = requester.getPhoneTags(number) 88 | return if (response.isNotBlank() && !JSONObject(response).has("error")) { 89 | val tags = JSONObject(response).getJSONObject("result").getJSONArray("tags") 90 | Array(tags.length()) { tags.getJSONObject(it).getString("tag") } 91 | } else emptyArray() 92 | } 93 | 94 | private fun findAllInfoByPhone(number: String): PhoneInfo { 95 | val phoneInfo = findNameByPhone(number) 96 | val tags = findTagsByPhone(number) 97 | return if (tags.isNotEmpty()) { 98 | PhoneInfo(number = phoneInfo.number, 99 | name = phoneInfo.name, 100 | isSpam = phoneInfo.isSpam, 101 | image = phoneInfo.image, 102 | tags = phoneInfo.tags + tags.take(5)) 103 | } else phoneInfo 104 | } 105 | 106 | private fun saveImage(url: String): String { 107 | val filename = File.createTempFile("profileImage", ".jpg") 108 | Fuel.download(url) 109 | .fileDestination { _, _ -> filename } 110 | .response { _ -> } 111 | return filename.path 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/GetContact/Requester.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api.GetContact 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.github.kittinunf.fuel.Fuel 6 | import com.github.kittinunf.fuel.core.Response 7 | import com.leti.phonedetector.LOG_TAG_VERBOSE 8 | import com.leti.phonedetector.model.Token 9 | import java.nio.charset.Charset 10 | import org.json.JSONObject 11 | 12 | class Requester(val context: Context, var token: Token, val timeout: Int) { 13 | 14 | private var cipherAES: AESCipher = AESCipher(token) 15 | private var timestamp: String = updateTimestamp() 16 | private val baseUrl = BASE_URL 17 | private var headers: MutableMap = mutableMapOf() 18 | private var requestData: MutableMap = mutableMapOf() 19 | private val methods: Map = mapOf( 20 | "number-detail" to "details", 21 | "search" to "search", 22 | "verify-code" to "", 23 | "register" to "") 24 | 25 | init { 26 | updateHeaders() 27 | } 28 | 29 | private fun updateHeaders() { 30 | headers = mutableMapOf( 31 | "X-App-Version" to APP_VERSION, 32 | "X-Token" to token.token, 33 | "X-Os" to token.androidOS, 34 | "X-Client-Device-Id" to token.deviceId, 35 | "Content-Type" to "application/json; charset=utf-8", 36 | "Connection" to "close", 37 | "Accept-Encoding" to "gzip, deflate", 38 | "X-Req-Timestamp" to timestamp, 39 | "X-Req-Signature" to "", 40 | "X-Encrypted" to "1" 41 | ) 42 | 43 | requestData = mutableMapOf( 44 | "countryCode" to COUNTRY, 45 | "source" to "", 46 | "token" to token.token 47 | ) 48 | } 49 | 50 | fun updateToken(token_: Token) { 51 | token = token_ 52 | cipherAES.updateToken(token_) 53 | timestamp = updateTimestamp() 54 | updateHeaders() 55 | } 56 | 57 | private fun updateTimestamp(): String { 58 | return System.currentTimeMillis().toString() 59 | } 60 | 61 | private fun preparePayload(data: MutableMap): String { 62 | return JSONObject(data as Map<*, *>).toString().replace("[ ~]".toRegex(), "") 63 | } 64 | 65 | private fun sendPost(url: String, data: String): Pair { 66 | Log.d(LOG_TAG_VERBOSE, "Header $headers, data: $data") 67 | val (_, response, _) = Fuel.post(url).header(headers).body(data).timeout(timeout * 1000 + 1).response() 68 | Log.d(LOG_TAG_VERBOSE, "${response.statusCode}, ${response.data.toString(Charset.defaultCharset())}") 69 | timestamp = updateTimestamp() 70 | return parseResponse(response) 71 | } 72 | 73 | private fun sendRequestEncrypted(url: String, data: String): Pair { 74 | headers["X-Encrypted"] = "1" 75 | return sendPost(url, JSONObject(mapOf("data" to cipherAES.encryptAESWithBase64(data))).toString()) 76 | } 77 | 78 | private fun sendRequestNoEncrypted(url: String, data: String): Pair { 79 | headers["X-Encrypted"] = "0" 80 | return sendPost(url, data) 81 | } 82 | 83 | private fun parseResponse(response: Response): Pair { 84 | Log.d(LOG_TAG_VERBOSE, "${response.statusCode} code") 85 | return when (response.statusCode) { 86 | 200 -> Pair(true, JSONObject(response.data.toString(Charset.defaultCharset())).getString("data")) 87 | 201 -> Pair(true, response.data.toString(Charset.defaultCharset())) 88 | 404 -> Pair(false, response.data.toString()) 89 | else -> { 90 | val responseText = JSONObject(response.data.toString(Charset.defaultCharset())).getString("data") 91 | val responseDecrypted = cipherAES.decryptAESWithBase64(responseText.toString()) 92 | val errorCode = JSONObject(responseDecrypted).getJSONObject("meta").getString("errorCode") 93 | Log.d(LOG_TAG_VERBOSE, "Error in parseResponse: $errorCode, $responseDecrypted") 94 | 95 | // TODO captcha bypass 96 | when (errorCode) { 97 | "403004" -> { Pair(false, response.data.toString(Charset.defaultCharset())) } 98 | else -> { 99 | Pair(false, response.data.toString(Charset.defaultCharset())) 100 | } 101 | } 102 | Pair(false, response.data.toString(Charset.defaultCharset())) 103 | } 104 | } 105 | } 106 | 107 | private fun sendReqToTheServer(url: String, payload: MutableMap, noEncryption: Boolean = false): String { 108 | val payloadPrepared = preparePayload(payload) 109 | Log.d(LOG_TAG_VERBOSE, "Payload: $payloadPrepared, $timestamp") 110 | headers["X-Req-Signature"] = cipherAES.createSignature(payloadPrepared, timestamp) 111 | 112 | val (isOk, response) = if (noEncryption) { sendRequestNoEncrypted(url, payloadPrepared) } else { sendRequestEncrypted(url, payloadPrepared) } 113 | 114 | return if (isOk) cipherAES.decryptAESWithBase64(response) else JSONObject(mapOf("error" to response)).toString() 115 | } 116 | 117 | fun getPhoneName(number: String): String { 118 | val method = "search" 119 | requestData["source"] = methods[method].toString() 120 | requestData["phoneNumber"] = number 121 | return sendReqToTheServer("$baseUrl/$API_VERSION/$method", requestData) 122 | } 123 | 124 | fun getPhoneTags(number: String): String { 125 | val method = "number-detail" 126 | requestData["source"] = methods[method].toString() 127 | requestData["phoneNumber"] = number 128 | return sendReqToTheServer("$baseUrl/$API_VERSION/$method", requestData) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/GetContact/config.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api.GetContact 2 | 3 | const val APP_VERSION = "5.6.2" 4 | const val BASE_URL = "https://pbssrv-centralevents.com" 5 | const val API_VERSION = "v2.8" 6 | const val COUNTRY = "RU" 7 | const val HMAC_KEY = "y1gY|J%&6V kTi\$>_Ali8]/xCqmMMP1\$*)I8FwJ,*r_YUM 4h?@7+@#<>+w-e3VW" 8 | const val MOD_EXP = 900719925481 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/api/NeberitrubkuAPI/NeberitrubkuAPI.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.api 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.AsyncTask 5 | import android.util.Log 6 | import com.leti.phonedetector.LOG_TAG_ERROR 7 | import com.leti.phonedetector.model.PhoneInfo 8 | import org.jsoup.Jsoup 9 | import org.jsoup.select.Elements 10 | 11 | class NeberitrubkuAPI(number_: String, val timeout: Int) { 12 | var number: String 13 | val url: String = "https://www.neberitrubku.ru/nomer-telefona/" 14 | 15 | init { 16 | number = convertPhoneToAPI(number_) 17 | } 18 | 19 | private fun convertPhoneToAPI(number: String): String { 20 | if (number.startsWith("+7")) { 21 | return number.replace("+7", "8") 22 | } else { 23 | return number 24 | } 25 | } 26 | 27 | private fun convertPhoneDefault(number: String): String { 28 | if (number.startsWith("8")) { 29 | return number.replace("^8".toRegex(), "+7") 30 | } else { 31 | return number 32 | } 33 | } 34 | 35 | fun getUser(): PhoneInfo { 36 | return NetworkTask().execute(url + number).get() 37 | } 38 | 39 | fun findInfo(): PhoneInfo { 40 | val doc = Jsoup.connect("$url/$number").timeout(timeout * 1000 + 1).get() 41 | val categories = doc.select("div.categories") 42 | val ratings = doc.select("div.ratings") 43 | val comments = doc.select("span.review_comment") 44 | 45 | val name: String? = parseCategories(categories) 46 | val rating: String? = parseRating(ratings) 47 | val tags: Array = parseComments(comments) 48 | 49 | val isSpam = rating?.contains("отриц") ?: false 50 | 51 | val user = if (name == null || rating == null) PhoneInfo( 52 | number = convertPhoneDefault(number) 53 | ) 54 | else PhoneInfo( 55 | number = convertPhoneDefault( 56 | number 57 | ), name = "$name", tags = tags, isSpam = isSpam 58 | ) 59 | 60 | return user 61 | } 62 | 63 | private fun parseCategories(categories: Elements): String? { 64 | val resultCategories = ArrayList() 65 | for (cat in categories) { 66 | val catRaw = cat.select("li.active") 67 | resultCategories.add(catRaw.text().replace("\\d+x ".toRegex(), "")) 68 | } 69 | return if (resultCategories.size > 0) resultCategories[0] else null 70 | } 71 | 72 | private fun parseRating(ratings: Elements): String? { 73 | val resultRating = ArrayList() 74 | for (rat in ratings) { 75 | val ratRaw = rat.select("li.active") 76 | resultRating.add(ratRaw.text().replace("\\d+x ".toRegex(), "")) 77 | } 78 | return if (resultRating.size > 0) resultRating[0] else null 79 | } 80 | 81 | private fun parseComments(comments: Elements): Array { 82 | val resultComments = ArrayList() 83 | for (comment in comments) { 84 | var commentText = comment.text() 85 | if (commentText.length >= 3 && !commentText.contains("Этот комментарий был")) { 86 | commentText = if (commentText.length < 40) commentText else commentText.substring(0, 37) + "..." 87 | resultComments.add(commentText) 88 | } 89 | } 90 | return resultComments.toSet().toList().sortedWith(compareBy { it.length }).take(5).toTypedArray() 91 | } 92 | 93 | @SuppressLint("StaticFieldLeak") 94 | inner class NetworkTask : AsyncTask() { 95 | 96 | override fun doInBackground(vararg parts: String): PhoneInfo { 97 | return try { 98 | this@NeberitrubkuAPI.findInfo() 99 | } catch (e: Exception) { 100 | Log.d(LOG_TAG_ERROR, "Error on API: $e") 101 | PhoneInfo( 102 | number = convertPhoneDefault( 103 | number 104 | ) 105 | ) 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/bitmap/BitmapReader.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.bitmap 2 | 3 | import android.content.ContentValues 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.provider.ContactsContract 7 | import java.io.ByteArrayOutputStream 8 | 9 | class BitmapReader { 10 | private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { 11 | val height = options.outHeight 12 | val width = options.outWidth 13 | var inSampleSize = 1 14 | if (height > reqHeight || width > reqWidth) { 15 | val heightRatio = 16 | Math.round(height.toFloat() / reqHeight.toFloat()) 17 | val widthRatio = 18 | Math.round(width.toFloat() / reqWidth.toFloat()) 19 | inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio 20 | } 21 | return inSampleSize 22 | } 23 | 24 | fun decodeFromResource(res: String, reqWidth: Int, reqHeight: Int): Bitmap { 25 | val options = BitmapFactory.Options() 26 | options.inJustDecodeBounds = true 27 | BitmapFactory.decodeFile(res, options) 28 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) 29 | options.inJustDecodeBounds = false 30 | return BitmapFactory.decodeFile(res, options) 31 | } 32 | 33 | fun readFile(res: String): ArrayList { 34 | val data = ArrayList() 35 | val bit = decodeFromResource(res, 512, 512) 36 | 37 | val stream = ByteArrayOutputStream() 38 | bit.compress(Bitmap.CompressFormat.PNG, 100, stream) 39 | val byteArray: ByteArray = stream.toByteArray() 40 | bit.recycle() 41 | 42 | val row = ContentValues() 43 | row.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) 44 | row.put(ContactsContract.CommonDataKinds.Photo.PHOTO, byteArray) 45 | data.add(row) 46 | return data 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/config.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector 2 | 3 | const val APP = "PHONEDETECTOR" 4 | const val APP_LOWERCASE = "phonedetector" 5 | 6 | const val APP_PREFERENCES = "${APP}_PREFERENCES" 7 | const val CHANNEL_ID = "${APP}_CHANNEL_ID" 8 | const val LOG_TAG_VERBOSE = "${APP}_VERBOSE" 9 | const val LOG_TAG_ERROR = "${APP}_ERROR" 10 | const val DEFAULT_DB_NAME = "$APP_LOWERCASE.db" 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/contacts/Contacts.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.contacts 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.net.Uri 7 | import android.provider.ContactsContract 8 | import androidx.core.content.ContextCompat 9 | 10 | class Contacts(val context: Context) { 11 | fun getContactNameByPhone(phoneNumber: String?): String? { 12 | if (!isPermissionGranted(context)) return null 13 | 14 | val uri = Uri.withAppendedPath( 15 | ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 16 | Uri.encode(phoneNumber) 17 | ) 18 | var contactName: String? = null 19 | val cursor = context.contentResolver.query(uri, arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME), null, null, null) 20 | if (cursor != null) { 21 | if (cursor.moveToFirst()) { 22 | contactName = cursor.getString(0) 23 | } 24 | cursor.close() 25 | } 26 | return contactName 27 | } 28 | 29 | private fun isPermissionGranted(context: Context): Boolean { 30 | return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/database/DBContract.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.database 2 | 3 | import android.provider.BaseColumns 4 | 5 | object DBContract { 6 | 7 | /* Inner class that defines the table contents */ 8 | class PhoneInfoEntry : BaseColumns { 9 | companion object { 10 | const val TABLE_NAME = "info" 11 | const val COLUMN_INFO_PHONE_NAME = "name" 12 | const val COLUMN_INFO_PHONE_NUMBER = "number" 13 | const val COLUMN_INFO_PHONE_IS_SPAM = "isSpam" 14 | const val COLUMN_INFO_PHONE_IMAGE = "image" 15 | } 16 | } 17 | 18 | class PhoneLogEntry : BaseColumns { 19 | companion object { 20 | const val TABLE_NAME = "phone_log" 21 | const val COLUMN_LOG_PHONE_NUMBER = "number" 22 | const val COLUMN_LOG_PHONE_TIME = "time" 23 | const val COLUMN_LOG_PHONE_DATE = "date" 24 | } 25 | } 26 | 27 | class PhoneLogTagsEntry : BaseColumns { 28 | companion object { 29 | const val TABLE_NAME = "tags" 30 | const val COLUMN_PHONE_LOG_TAGS_NUMBER = "number" 31 | const val COLUMN_PHONE_LOG_TAGS_TAG = "tag" 32 | } 33 | } 34 | 35 | class TokenEntry : BaseColumns { 36 | companion object { 37 | const val TABLE_NAME = "tokens" 38 | const val COLUMN_AES_KEY = "aes_key" 39 | const val COLUMN_ANDROID_OS = "android_os" 40 | const val COLUMN_DEVICE_ID = "device_id" 41 | const val COLUMN_IS_ACTIVE = "is_active" 42 | const val COLUMN_PRIVATE_KEY = "private_key" 43 | const val COLUMN_REMAIN_COUNT = "remain_count" 44 | const val COLUMN_TOKEN = "token" 45 | const val COLUMN_IS_PRIMARY_USE = "is_primary_use" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/database/TokenDBHelper.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.database 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.database.Cursor 7 | import android.database.sqlite.SQLiteConstraintException 8 | import android.database.sqlite.SQLiteDatabase 9 | import android.database.sqlite.SQLiteException 10 | import android.database.sqlite.SQLiteOpenHelper 11 | import android.util.Log 12 | import com.leti.phonedetector.* 13 | import com.leti.phonedetector.model.Token 14 | import kotlin.collections.ArrayList 15 | 16 | class TokenDBHelper(val context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { 17 | override fun onCreate(db: SQLiteDatabase) { 18 | Log.d(LOG_TAG_VERBOSE, "Call onCreate class TokenLogDBHelper") 19 | 20 | db.execSQL(create_token_table) 21 | } 22 | 23 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 24 | Log.d(LOG_TAG_VERBOSE, "Call onUpgrade onCreate class TokenLogDBHelper") 25 | cleanTables(db) 26 | } 27 | 28 | private fun cleanTables(db: SQLiteDatabase) { 29 | db.execSQL(drop_tokens) 30 | 31 | onCreate(db) 32 | } 33 | 34 | fun cleanTables() { 35 | val db = writableDatabase 36 | cleanTables(db) 37 | db.close() 38 | } 39 | 40 | override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 41 | onUpgrade(db, oldVersion, newVersion) 42 | } 43 | 44 | @Throws(SQLiteConstraintException::class) 45 | fun insertToken(token: Token): Boolean { 46 | Log.d(LOG_TAG_VERBOSE, "Call insertToken with param Token: '${token.token}'") 47 | 48 | val foundToken = this.findToken(token.token) 49 | Log.d(LOG_TAG_VERBOSE, "Found token: ${foundToken?.token}") 50 | 51 | if (foundToken != null) { 52 | this.deleteToken(token.token) 53 | } 54 | 55 | val db = writableDatabase 56 | 57 | val valuesInfo = ContentValues() 58 | valuesInfo.put(DBContract.TokenEntry.COLUMN_TOKEN, token.token) 59 | valuesInfo.put(DBContract.TokenEntry.COLUMN_AES_KEY, token.aesKey) 60 | valuesInfo.put(DBContract.TokenEntry.COLUMN_ANDROID_OS, token.androidOS) 61 | valuesInfo.put(DBContract.TokenEntry.COLUMN_DEVICE_ID, token.deviceId) 62 | valuesInfo.put(DBContract.TokenEntry.COLUMN_IS_ACTIVE, token.isActive) 63 | valuesInfo.put(DBContract.TokenEntry.COLUMN_PRIVATE_KEY, token.privateKey) 64 | valuesInfo.put(DBContract.TokenEntry.COLUMN_REMAIN_COUNT, token.remainCount) 65 | valuesInfo.put(DBContract.TokenEntry.COLUMN_IS_PRIMARY_USE, token.isPrimaryUse) 66 | db.insert(DBContract.TokenEntry.TABLE_NAME, null, valuesInfo) 67 | 68 | db.close() 69 | return true 70 | } 71 | 72 | @Throws(SQLiteConstraintException::class) 73 | fun deleteToken(token: String): Boolean { 74 | Log.d(LOG_TAG_VERBOSE, "Call deleteToken with param token: '$token'") 75 | 76 | val db = writableDatabase 77 | // TODO SQL Injection 78 | db.execSQL("DELETE FROM ${DBContract.TokenEntry.TABLE_NAME} WHERE ${DBContract.TokenEntry.COLUMN_TOKEN} = '$token'") 79 | db.close() 80 | return true 81 | } 82 | 83 | @SuppressLint("Recycle") 84 | @Throws(SQLiteConstraintException::class) 85 | fun findToken(tokenInput: String): Token? { 86 | // TODO SQL Injection 87 | Log.d(LOG_TAG_VERBOSE, "Call findToken with param token: '$tokenInput'") 88 | 89 | val tokens = ArrayList() 90 | val db = readableDatabase 91 | 92 | try { 93 | val cursor = db.rawQuery("SELECT * FROM ${DBContract.TokenEntry.TABLE_NAME} WHERE " + 94 | "${DBContract.TokenEntry.TABLE_NAME}.${DBContract.TokenEntry.COLUMN_TOKEN} " + 95 | "= \"${tokenInput}\";", null) 96 | 97 | if (cursor!!.moveToFirst()) { 98 | while (!cursor.isAfterLast) { 99 | tokens.add(parseCursor(cursor)) 100 | cursor.moveToNext() 101 | } 102 | } 103 | } catch (e: SQLiteException) { 104 | Log.e(LOG_TAG_ERROR, "Error in findToken: $e") 105 | db.execSQL(SQL_CREATE_ENTRIES) 106 | db.close() 107 | return null 108 | } 109 | 110 | db.close() 111 | return if (tokens.size > 0) tokens[0] else null 112 | } 113 | 114 | fun getTokens(): ArrayList { 115 | // TODO SQL Injection 116 | Log.d(LOG_TAG_VERBOSE, "Call getTokens") 117 | 118 | val tokens = ArrayList() 119 | val db = readableDatabase 120 | 121 | try { 122 | val cursor = db.rawQuery("SELECT * FROM ${DBContract.TokenEntry.TABLE_NAME};", null) 123 | 124 | if (cursor!!.moveToFirst()) { 125 | while (!cursor.isAfterLast) { 126 | tokens.add(parseCursor(cursor)) 127 | cursor.moveToNext() 128 | } 129 | } 130 | } catch (e: SQLiteException) { 131 | Log.e(LOG_TAG_ERROR, "Error in getTokens: $e") 132 | db.execSQL(SQL_CREATE_ENTRIES) 133 | db.close() 134 | return tokens 135 | } 136 | 137 | db.close() 138 | return tokens 139 | } 140 | 141 | @Throws(SQLiteConstraintException::class) 142 | fun updateToken(token: Token) { 143 | val db = this.writableDatabase 144 | val contentValues = ContentValues() 145 | contentValues.put(DBContract.TokenEntry.COLUMN_AES_KEY, token.aesKey) 146 | contentValues.put(DBContract.TokenEntry.COLUMN_ANDROID_OS, token.androidOS) 147 | contentValues.put(DBContract.TokenEntry.COLUMN_DEVICE_ID, token.deviceId) 148 | contentValues.put(DBContract.TokenEntry.COLUMN_IS_ACTIVE, token.isActive) 149 | contentValues.put(DBContract.TokenEntry.COLUMN_PRIVATE_KEY, token.privateKey) 150 | contentValues.put(DBContract.TokenEntry.COLUMN_REMAIN_COUNT, token.remainCount) 151 | contentValues.put(DBContract.TokenEntry.COLUMN_TOKEN, token.token) 152 | contentValues.put(DBContract.TokenEntry.COLUMN_IS_PRIMARY_USE, token.isPrimaryUse) 153 | 154 | db.update(DBContract.TokenEntry.TABLE_NAME, contentValues, "token = ?", arrayOf(token.token)) 155 | db.close() 156 | } 157 | 158 | private fun parseCursor(cursor: Cursor): Token { 159 | val aesKey = cursor.getString(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_AES_KEY)) 160 | val androidOS = cursor.getString(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_ANDROID_OS)) 161 | val deviceId = cursor.getString(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_DEVICE_ID)) 162 | val isActive = cursor.getInt(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_IS_ACTIVE)) != 0 163 | val privateKey = cursor.getInt(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_PRIVATE_KEY)) 164 | val remainCount = cursor.getInt(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_REMAIN_COUNT)) 165 | val token = cursor.getString(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_TOKEN)) 166 | val isPrimaryUse = cursor.getInt(cursor.getColumnIndex(DBContract.TokenEntry.COLUMN_IS_PRIMARY_USE)) != 0 167 | 168 | return Token(token, aesKey, androidOS, deviceId, isActive, privateKey, remainCount, isPrimaryUse) 169 | } 170 | 171 | companion object { 172 | const val DATABASE_VERSION = 1 173 | const val DATABASE_NAME = DEFAULT_DB_NAME 174 | 175 | private val create_token_table = "CREATE TABLE IF NOT EXISTS ${DBContract.TokenEntry.TABLE_NAME} ( " + 176 | "${DBContract.TokenEntry.COLUMN_AES_KEY} TEXT, " + 177 | "${DBContract.TokenEntry.COLUMN_ANDROID_OS } TEXT, " + 178 | "${DBContract.TokenEntry.COLUMN_DEVICE_ID} TEXT, " + 179 | "${DBContract.TokenEntry.COLUMN_IS_ACTIVE} INTEGER, " + 180 | "${DBContract.TokenEntry.COLUMN_PRIVATE_KEY} INTEGER, " + 181 | "${DBContract.TokenEntry.COLUMN_REMAIN_COUNT} INTEGER, " + 182 | "${DBContract.TokenEntry.COLUMN_TOKEN} TEXT PRIMARY KEY, " + 183 | "${DBContract.TokenEntry.COLUMN_IS_PRIMARY_USE} INTEGER);" 184 | 185 | private val SQL_CREATE_ENTRIES = create_token_table 186 | 187 | private val drop_tokens = "DROP TABLE IF EXISTS ${DBContract.TokenEntry.TABLE_NAME};" 188 | 189 | private val SQL_DELETE_ENTRIES = drop_tokens 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/model/PhoneInfo.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.model 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | const val DEFAULT_NAME = "Undefined user" 7 | const val DEFAULT_NUMBER = "+7800553535" 8 | const val DEFAULT_IS_SPAM_STATE = false 9 | const val DEFAULT_IMAGE = "empty" 10 | const val DEFAULT_TIME = "23:59:59" 11 | const val DEFAULT_DATE = "1970.01.01" 12 | val DEFAULT_TAGS: Array = emptyArray() 13 | 14 | // Information about phone number 15 | class PhoneInfo( 16 | val name: String = DEFAULT_NAME, 17 | val number: String = DEFAULT_NUMBER, 18 | val isSpam: Boolean = DEFAULT_IS_SPAM_STATE, 19 | val tags: Array = DEFAULT_TAGS, 20 | val image: String = DEFAULT_IMAGE 21 | ) : Parcelable { 22 | constructor(parcel: Parcel) : this( 23 | parcel.readString().toString(), 24 | parcel.readString().toString(), 25 | parcel.readByte() != 0.toByte(), 26 | parcel.createStringArray() as Array, 27 | parcel.readString().toString() 28 | ) 29 | 30 | fun isDefault(): Boolean { 31 | return name == DEFAULT_NAME && 32 | isSpam == DEFAULT_IS_SPAM_STATE && 33 | tags.contentEquals(DEFAULT_TAGS) && 34 | image == DEFAULT_IMAGE 35 | } 36 | 37 | override fun writeToParcel(parcel: Parcel, flags: Int) { 38 | parcel.writeString(name) 39 | parcel.writeString(number) 40 | parcel.writeByte(if (isSpam) 1 else 0) 41 | parcel.writeStringArray(tags) 42 | parcel.writeString(image) 43 | } 44 | 45 | override fun describeContents(): Int { 46 | return 0 47 | } 48 | 49 | override fun equals(other: Any?): Boolean { 50 | if (this === other) return true 51 | if (javaClass != other?.javaClass) return false 52 | 53 | other as PhoneInfo 54 | 55 | if (name != other.name) return false 56 | if (number != other.number) return false 57 | if (isSpam != other.isSpam) return false 58 | if (!tags.contentEquals(other.tags)) return false 59 | if (image != other.image) return false 60 | 61 | return true 62 | } 63 | 64 | override fun hashCode(): Int { 65 | var result = name.hashCode() 66 | result = 31 * result + number.hashCode() 67 | result = 31 * result + isSpam.hashCode() 68 | result = 31 * result + tags.contentHashCode() 69 | result = 31 * result + image.hashCode() 70 | return result 71 | } 72 | 73 | override fun toString(): String { 74 | return "PhoneInfo {name:$name, number:$number, isSpam:$isSpam, tags:$tags, image:$image" 75 | } 76 | 77 | companion object CREATOR : Parcelable.Creator { 78 | override fun createFromParcel(parcel: Parcel): PhoneInfo { 79 | return PhoneInfo(parcel) 80 | } 81 | 82 | override fun newArray(size: Int): Array { 83 | return arrayOfNulls(size) 84 | } 85 | } 86 | } 87 | 88 | // Log element with info about phone number 89 | class PhoneLogInfo( 90 | val name: String = DEFAULT_NAME, 91 | val number: String = DEFAULT_NUMBER, 92 | val isSpam: Boolean = DEFAULT_IS_SPAM_STATE, 93 | val tags: Array = DEFAULT_TAGS, 94 | val time: String = DEFAULT_TIME, 95 | val date: String = DEFAULT_DATE, 96 | val image: String = DEFAULT_IMAGE 97 | ) : Parcelable { 98 | 99 | constructor(parcel: Parcel) : this(parcel.readString().toString(), 100 | parcel.readString().toString(), 101 | parcel.readByte() != 0.toByte(), 102 | parcel.createStringArray() as Array, 103 | parcel.readString().toString(), 104 | parcel.readString().toString(), 105 | parcel.readString().toString()) 106 | 107 | constructor(phoneInfo: PhoneInfo, time: String = DEFAULT_TIME, date: String = DEFAULT_DATE) : 108 | this(phoneInfo.name, phoneInfo.number, phoneInfo.isSpam, phoneInfo.tags, time, date, phoneInfo.image) 109 | 110 | fun toPhoneInfo(): PhoneInfo { 111 | return PhoneInfo( 112 | name, 113 | number, 114 | isSpam, 115 | tags, 116 | image 117 | ) 118 | } 119 | 120 | fun isDefault(): Boolean { 121 | return this.toPhoneInfo().isDefault() && time == DEFAULT_TIME && date == DEFAULT_DATE 122 | } 123 | 124 | override fun writeToParcel(parcel: Parcel, flags: Int) { 125 | } 126 | 127 | override fun describeContents(): Int { 128 | return 0 129 | } 130 | 131 | override fun equals(other: Any?): Boolean { 132 | if (this === other) return true 133 | if (javaClass != other?.javaClass) return false 134 | 135 | other as PhoneLogInfo 136 | 137 | if (name != other.name) return false 138 | if (number != other.number) return false 139 | if (isSpam != other.isSpam) return false 140 | if (!tags.contentEquals(other.tags)) return false 141 | if (time != other.time) return false 142 | if (date != other.date) return false 143 | if (image != other.image) return false 144 | 145 | return true 146 | } 147 | 148 | override fun hashCode(): Int { 149 | var result = name.hashCode() 150 | result = 31 * result + number.hashCode() 151 | result = 31 * result + isSpam.hashCode() 152 | result = 31 * result + tags.contentHashCode() 153 | result = 31 * result + time.hashCode() 154 | result = 31 * result + date.hashCode() 155 | result = 31 * result + image.hashCode() 156 | return result 157 | } 158 | 159 | companion object CREATOR : Parcelable.Creator { 160 | override fun createFromParcel(parcel: Parcel): PhoneLogInfo { 161 | return PhoneLogInfo(parcel) 162 | } 163 | 164 | override fun newArray(size: Int): Array { 165 | return arrayOfNulls(size) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/model/Token.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.model 2 | 3 | const val AES_KEY = "764fe50cdb21a07c4c049377754c2f50f127febb3aa67e03c7334f414e0fa7db" 4 | const val ANDROID_OS = "android 5.0" 5 | const val DEVICE_ID = "8edbe110a4079828" 6 | const val IS_ACTIVE = true 7 | const val PRIVATE_KEY = 3272978 8 | const val REMAIN_COUNT = 200 9 | const val TOKEN = "hphofd5757307f5dbffce25ae9ef4bd54dc56e770bc763215e7dc4f02e3" 10 | const val IS_PRIMARY_USE = false 11 | 12 | class Token( 13 | val token: String = TOKEN, 14 | val aesKey: String = AES_KEY, 15 | val androidOS: String = ANDROID_OS, 16 | val deviceId: String = DEVICE_ID, 17 | var isActive: Boolean = IS_ACTIVE, 18 | val privateKey: Int = PRIVATE_KEY, 19 | var remainCount: Int = REMAIN_COUNT, 20 | var isPrimaryUse: Boolean = IS_PRIMARY_USE 21 | ) { 22 | 23 | fun isValid(): Boolean { 24 | return isActive && remainCount > 0 25 | } 26 | 27 | fun isDefault(): Boolean { 28 | return token == TOKEN && 29 | aesKey == AES_KEY && 30 | androidOS == ANDROID_OS && 31 | deviceId == DEVICE_ID && 32 | privateKey == PRIVATE_KEY && 33 | isPrimaryUse == isPrimaryUse 34 | } 35 | 36 | fun updateRemainCount(remainCount_: Int) { 37 | remainCount = remainCount_ 38 | isActive = remainCount_ > 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/notification/BlockNotification.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.notification 2 | 3 | import android.app.AlarmManager 4 | import android.app.Notification 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.BitmapFactory 9 | import android.os.SystemClock 10 | import androidx.core.app.NotificationCompat 11 | import com.leti.phonedetector.CHANNEL_ID 12 | import com.leti.phonedetector.R 13 | import com.leti.phonedetector.model.PhoneInfo 14 | 15 | class BlockNotification(private val context: Context, private val intent: Intent, private val user: PhoneInfo) { 16 | 17 | fun createNotification(): Notification { 18 | val snoozePendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) 19 | 20 | val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply { 21 | setSmallIcon(R.drawable.ic_notification_icon) 22 | setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) 23 | setContentTitle("Don't forget to block incoming number") 24 | setContentText("${user.number} - ${user.name}") 25 | setContentIntent(snoozePendingIntent) 26 | setAutoCancel(true) 27 | } 28 | 29 | return builder.build() 30 | } 31 | 32 | private fun createScheduledPushUp(notification: Notification, delayTime: Int) { 33 | val notificationIntent = Intent(context, NotificationPublisher::class.java) 34 | notificationIntent.putExtra(NotificationPublisher.NOTIFICATION_ID, 1) 35 | notificationIntent.putExtra(NotificationPublisher.NOTIFICATION, notification) 36 | 37 | val pendingIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) 38 | 39 | val futureInMillis = SystemClock.elapsedRealtime() + delayTime * 60 * 1000 40 | val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 41 | alarmManager[AlarmManager.ELAPSED_REALTIME_WAKEUP, futureInMillis] = pendingIntent 42 | } 43 | 44 | fun notify(delayTime: Int) { 45 | val notification = createNotification() 46 | createScheduledPushUp(notification, delayTime) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/notification/IncomingNotification.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.notification 2 | 3 | import android.app.Notification 4 | import android.app.NotificationManager 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.BitmapFactory 9 | import androidx.core.app.NotificationCompat 10 | import com.leti.phonedetector.CHANNEL_ID 11 | import com.leti.phonedetector.R 12 | import com.leti.phonedetector.model.PhoneInfo 13 | 14 | class IncomingNotification(val context: Context, val intent: Intent, val phone: PhoneInfo) { 15 | 16 | private fun createNotification(): Notification { 17 | val snoozePendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) 18 | 19 | val bigText = NotificationCompat.BigTextStyle() 20 | bigText.bigText(phone.number + "\n" + phone.tags.joinToString(separator = "\n")) 21 | bigText.setBigContentTitle(phone.name) 22 | 23 | val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply { 24 | setSmallIcon(R.drawable.ic_notification_icon) 25 | setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) 26 | setContentTitle(phone.name) 27 | setContentText(phone.number) 28 | if (phone.tags.isNotEmpty()) setStyle(bigText) 29 | setContentIntent(snoozePendingIntent) 30 | setAutoCancel(true) 31 | setSound(null) 32 | priority = NotificationCompat.PRIORITY_HIGH 33 | } 34 | 35 | return builder.build() 36 | } 37 | 38 | fun notifyNow() { 39 | val notification = createNotification() 40 | val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 41 | notificationManager.notify(1, notification) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/notification/NotificationPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.notification 2 | 3 | import android.app.Notification 4 | import android.app.NotificationManager 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | 9 | class NotificationPublisher : BroadcastReceiver() { 10 | override fun onReceive(context: Context, intent: Intent) { 11 | val notificationManager = 12 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 13 | val notification = 14 | intent.getParcelableExtra(NOTIFICATION) 15 | val id = intent.getIntExtra(NOTIFICATION_ID, 0) 16 | notificationManager.notify(id, notification) 17 | } 18 | 19 | companion object { 20 | var NOTIFICATION_ID = "notification-id" 21 | var NOTIFICATION = "notification" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/overlay/OverlayCreator.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.overlay 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import com.leti.phonedetector.OverlayActivity 6 | import com.leti.phonedetector.model.PhoneInfo 7 | 8 | class OverlayCreator(private val context: Context) { 9 | 10 | fun createIntent(user: PhoneInfo, isDisplayButtons: Boolean): Intent { 11 | val mIntent = Intent(context, OverlayActivity::class.java) 12 | mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 13 | mIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) 14 | mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 15 | mIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) 16 | mIntent.addCategory(Intent.CATEGORY_LAUNCHER) 17 | 18 | mIntent.putExtra("user", user) 19 | mIntent.putExtra("is_display_buttons", isDisplayButtons) 20 | return mIntent 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/leti/phonedetector/search/Search.kt: -------------------------------------------------------------------------------- 1 | package com.leti.phonedetector.search 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import android.telephony.PhoneNumberUtils 7 | import androidx.preference.PreferenceManager 8 | import com.leti.phonedetector.api.GetContact.GetContactAPI 9 | import com.leti.phonedetector.api.NeberitrubkuAPI 10 | import com.leti.phonedetector.database.PhoneLogDBHelper 11 | import com.leti.phonedetector.model.PhoneInfo 12 | import com.leti.phonedetector.model.PhoneLogInfo 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | 16 | class Search(private val context: Context) { 17 | val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) 18 | 19 | private fun findUserByNetwork(number: String, timeout: Int, time: String, date: String): PhoneLogInfo { 20 | val use_getcontact = sharedPreferences.getBoolean("use_getcontact", true) 21 | val use_neberitrubku = sharedPreferences.getBoolean("use_neberitrubku", true) 22 | 23 | val nebUser = if (use_neberitrubku) NeberitrubkuAPI(number, timeout).getUser() else PhoneInfo(number = number) 24 | if (!nebUser.isDefault()) 25 | return PhoneLogInfo( 26 | nebUser, 27 | time = time, 28 | date = date 29 | ) 30 | 31 | val getUser = if (use_getcontact) GetContactAPI(context, timeout).getAllByPhone(number) else PhoneInfo(number = number) 32 | 33 | val resultUser = 34 | if (nebUser.isDefault() && !getUser.isDefault()) { 35 | getUser 36 | } else if (!nebUser.isDefault() && getUser.isDefault()) { 37 | nebUser 38 | } else if (!nebUser.isDefault() && nebUser.isSpam) { 39 | nebUser 40 | } else { 41 | getUser 42 | } 43 | 44 | return PhoneLogInfo( 45 | resultUser, 46 | time = time, 47 | date = date 48 | ) 49 | } 50 | 51 | @SuppressLint("SimpleDateFormat") 52 | fun startPhoneDetection(incomingNumberRaw: String): PhoneLogInfo { 53 | val incomingNumber = this.formatE164NumberRU(incomingNumberRaw) 54 | val timeout = sharedPreferences.getInt("detection_delay_seekbar", 5) 55 | val isNetworkOnly = sharedPreferences.getBoolean("use_only_network_info", false) 56 | val noCacheEmpty = sharedPreferences.getBoolean("no_cache_empty_phones", true) 57 | 58 | val db = PhoneLogDBHelper(context) 59 | 60 | val date = SimpleDateFormat("yyyy.MM.dd").format(Date()) 61 | val time = SimpleDateFormat("HH:mm:ss").format(Date()) 62 | 63 | val user: PhoneLogInfo = if (isNetworkOnly) { 64 | this.findUserByNetwork(incomingNumber, timeout, time, date) 65 | } else { 66 | val foundUser: PhoneInfo? = db.findPhoneByNumber(incomingNumber) 67 | 68 | if (foundUser != null && !foundUser.isDefault()) { 69 | PhoneLogInfo(foundUser, time, date) 70 | } else { 71 | val foundUserNetwork = this.findUserByNetwork(incomingNumber, timeout, time, date) 72 | 73 | if (!foundUserNetwork.toPhoneInfo().isDefault()) { 74 | foundUserNetwork 75 | } else { 76 | PhoneLogInfo( 77 | number = incomingNumber, 78 | date = date, 79 | time = time 80 | ) 81 | } 82 | } 83 | } 84 | 85 | if (!noCacheEmpty || !user.toPhoneInfo().isDefault()) { 86 | db.insertPhone(user) 87 | } 88 | return user 89 | } 90 | 91 | fun formatE164NumberRU(number: String): String { 92 | return formatE164Number(number, "RU") 93 | } 94 | 95 | fun formatE164Number(phNum: String, countryCode: String): String { 96 | return PhoneNumberUtils.formatNumberToE164(phNum, countryCode) ?: phNum 97 | } 98 | 99 | fun findUserByPhone(number: String): PhoneInfo { 100 | val db = PhoneLogDBHelper(context) 101 | return db.findPhoneByNumber(number) ?: PhoneInfo(number = number) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable-hdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable-mdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable-xhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable/ic_empty_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty_user_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable/ic_spam.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spam_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spam_image_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spam_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/drawable/ic_spam_round.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 30 | 31 | 32 | 36 | 37 | 41 | 42 | 47 | 48 | 57 | 58 | 59 |