├── .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 | [](https://travis-ci.com/kovinevmv/PhoneDetector)
4 | [](https://app.codacy.com/manual/kovinevmv/PhoneDetector?utm_source=github.com&utm_medium=referral&utm_content=kovinevmv/PhoneDetector&utm_campaign=Badge_Grade_Dashboard)
5 | [](https://wakatime.com/badge/github/kovinevmv/PhoneDetector)
6 |
7 | ## App screens
8 |
9 | | | | |
10 | :---:|:---:|:---:
11 |  |  | 
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 |
67 |
68 |
69 |
77 |
78 |
85 |
86 |
90 |
91 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_statistics.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
17 |
18 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
39 |
40 |
46 |
47 |
52 |
53 |
54 |
59 |
60 |
66 |
67 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
28 | />
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/element_log.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
21 |
22 |
29 |
30 |
35 |
36 |
46 |
47 |
57 |
58 |
59 |
60 |
64 |
65 |
71 |
72 |
81 |
82 |
89 |
90 |
91 |
92 |
93 |
94 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/settings_activity.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/raw/darkglamour.js:
--------------------------------------------------------------------------------
1 | /**
2 | * AnyChart is lightweight robust charting library with great API and Docs, that works with your stack and has tons of chart types and features.
3 | *
4 | * Theme: darkGlamour
5 | * Version: 2.0.0 (2019-04-26)
6 | * License: https://www.anychart.com/buy/
7 | * Contact: sales@anychart.com
8 | * Copyright: AnyChart.com 2019. All rights reserved.
9 | */
10 | (function() {
11 | "use strict";
12 |
13 | function a() {
14 | return window.anychart.color.setOpacity(this.sourceColor, 0.6, !0);
15 | }
16 | function b() {
17 | return window.anychart.color.darken(this.sourceColor);
18 | }
19 | function c() {
20 | return window.anychart.color.lighten(this.sourceColor);
21 | }
22 | var e = {
23 | palette: {
24 | type: "distinct",
25 | items: "#D81B60 #7b5881 #ab47bc #d81b60 #880e4f #ffd600 #ff6e40 #03a9f4 #5e35b1 #1976d2".split(
26 | " "
27 | )
28 | },
29 | defaultOrdinalColorScale: {
30 | autoColors: function(d) {
31 | return window.anychart.color.blendedHueProgression(
32 | "#D81B60",
33 | "#d81b60",
34 | d
35 | );
36 | }
37 | },
38 | defaultLinearColorScale: { colors: ["#D81B60", "#d81b60"] },
39 | defaultFontSettings: {
40 | fontFamily: '"Source Sans Pro", sans-serif',
41 | fontSize: 13,
42 | fontColor: "#d7cacc"
43 | },
44 | defaultBackground: {
45 | fill: "#303030",
46 | stroke: "#192125",
47 | cornerType: "round",
48 | corners: 0
49 | },
50 | defaultAxis: {
51 | stroke: "#655B66",
52 | title: { fontSize: 15 },
53 | ticks: { stroke: "#655B66" },
54 | minorTicks: { stroke: "#46474F" }
55 | },
56 | defaultGridSettings: { stroke: "#655B66" },
57 | defaultMinorGridSettings: { stroke: "#46474F" },
58 | defaultSeparator: { fill: "#84707C" },
59 | defaultTooltip: {
60 | background: { fill: "#303030 0.9", stroke: "2 #192125", corners: 3 },
61 | fontSize: 13,
62 | title: { align: "center", fontSize: 15 },
63 | padding: { top: 10, right: 15, bottom: 10, left: 15 },
64 | separator: { margin: { top: 10, right: 10, bottom: 10, left: 10 } }
65 | },
66 | defaultColorRange: {
67 | stroke: "#455a64",
68 | ticks: { stroke: "#455a64", position: "outside", length: 7, enabled: !0 },
69 | minorTicks: {
70 | stroke: "#455a64",
71 | position: "outside",
72 | length: 5,
73 | enabled: !0
74 | },
75 | marker: {
76 | padding: { top: 3, right: 3, bottom: 3, left: 3 },
77 | fill: "#d7cacc"
78 | }
79 | },
80 | defaultScroller: {
81 | fill: "#5d5d5d",
82 | selectedFill: "#455a64",
83 | thumbs: {
84 | fill: "#546e7a",
85 | stroke: "#5d5d5d",
86 | hovered: { fill: "#78909c", stroke: "#455a64" }
87 | }
88 | },
89 | defaultLegend: { fontSize: 13 },
90 | chart: {
91 | defaultSeriesSettings: {
92 | base: {
93 | selected: {
94 | stroke: "1.5 #fafafa",
95 | markers: { stroke: "1.5 #fafafa" }
96 | }
97 | },
98 | lineLike: { selected: { stroke: "3 #fafafa" } },
99 | areaLike: { selected: { stroke: "3 #fafafa" } },
100 | marker: { selected: { stroke: "1.5 #fafafa" } },
101 | candlestick: {
102 | normal: {
103 | risingFill: "#D81B60",
104 | risingStroke: "#D81B60",
105 | fallingFill: "#d81b60",
106 | fallingStroke: "#d81b60"
107 | },
108 | hovered: {
109 | risingFill: c,
110 | risingStroke: b,
111 | fallingFill: c,
112 | fallingStroke: b
113 | },
114 | selected: {
115 | risingStroke: "3 #D81B60",
116 | fallingStroke: "3 #d81b60",
117 | risingFill: "#333333 0.85",
118 | fallingFill: "#333333 0.85"
119 | }
120 | },
121 | ohlc: {
122 | normal: { risingStroke: "#D81B60", fallingStroke: "#d81b60" },
123 | hovered: { risingStroke: b, fallingStroke: b },
124 | selected: { risingStroke: "3 #D81B60", fallingStroke: "3 #d81b60" }
125 | }
126 | },
127 | title: { fontSize: 17 },
128 | padding: { top: 20, right: 25, bottom: 15, left: 15 }
129 | },
130 | cartesianBase: {
131 | defaultSeriesSettings: {
132 | box: {
133 | selected: {
134 | medianStroke: "#fafafa",
135 | stemStroke: "#fafafa",
136 | whiskerStroke: "#fafafa",
137 | outlierMarkers: {
138 | enabled: null,
139 | size: 4,
140 | fill: "#fafafa",
141 | stroke: "#fafafa"
142 | }
143 | }
144 | }
145 | }
146 | },
147 | pieFunnelPyramidBase: {
148 | normal: { labels: { fontColor: null } },
149 | selected: { stroke: "1.5 #fafafa" },
150 | connectorStroke: "#84707C",
151 | outsideLabels: { autoColor: "#d7cacc" },
152 | insideLabels: { autoColor: "#5d5d5d" }
153 | },
154 | map: {
155 | unboundRegions: { enabled: !0, fill: "#5d5d5d", stroke: "#455a64" },
156 | defaultSeriesSettings: {
157 | base: {
158 | normal: { stroke: c, labels: { fontColor: "#212121" } },
159 | hovered: { fill: "#bdbdbd" },
160 | selected: { fill: "3 #fafafa" }
161 | },
162 | connector: {
163 | normal: { markers: { stroke: "1.5 #5d5d5d" } },
164 | hovered: { markers: { stroke: "1.5 #5d5d5d" } },
165 | selected: {
166 | stroke: "1.5 #fafafa",
167 | markers: { fill: "#fafafa", stroke: "1.5 #5d5d5d" }
168 | }
169 | },
170 | marker: { normal: { labels: { fontColor: "#d7cacc" } } }
171 | }
172 | },
173 | sparkline: {
174 | padding: 0,
175 | background: { stroke: "#303030" },
176 | defaultSeriesSettings: {
177 | area: { stroke: "1.5 #D81B60", fill: "#D81B60 0.5" },
178 | column: { fill: "#D81B60", negativeFill: "#d81b60" },
179 | line: { stroke: "1.5 #D81B60" },
180 | winLoss: { fill: "#D81B60", negativeFill: "#d81b60" }
181 | }
182 | },
183 | bullet: {
184 | background: { stroke: "#303030" },
185 | defaultMarkerSettings: { fill: "#D81B60", stroke: "2 #D81B60" },
186 | padding: { top: 5, right: 10, bottom: 5, left: 10 },
187 | margin: { top: 0, right: 0, bottom: 0, left: 0 },
188 | rangePalette: {
189 | items: ["#4D6570", "#445963", "#3B4D56", "#34444C", "#2D3B42"]
190 | }
191 | },
192 | heatMap: {
193 | normal: { stroke: "1 #303030", labels: { fontColor: "#212121" } },
194 | hovered: { stroke: "1.5 #303030" },
195 | selected: { stroke: "2 #fafafa", labels: { fontColor: "#fafafa" } }
196 | },
197 | treeMap: {
198 | normal: {
199 | headers: {
200 | background: { enabled: !0, fill: "#5d5d5d", stroke: "#455a64" }
201 | },
202 | labels: { fontColor: "#212121" },
203 | stroke: "#455a64"
204 | },
205 | hovered: {
206 | headers: {
207 | fontColor: "#d7cacc",
208 | background: { fill: "#455a64", stroke: "#455a64" }
209 | }
210 | },
211 | selected: { labels: { fontColor: "#fafafa" }, stroke: "2 #eceff1" }
212 | },
213 | stock: {
214 | padding: [20, 30, 20, 60],
215 | defaultPlotSettings: {
216 | xAxis: { background: { fill: "#655B66 0.3", stroke: "#655B66" } }
217 | },
218 | scroller: {
219 | fill: "none",
220 | selectedFill: "#655B66 0.3",
221 | outlineStroke: "#655B66",
222 | defaultSeriesSettings: {
223 | base: { selected: { stroke: a, fill: a } },
224 | lineLike: { selected: { stroke: a } },
225 | areaLike: { selected: { stroke: a, fill: a } },
226 | marker: { selected: { stroke: a } },
227 | candlestick: {
228 | normal: {
229 | risingFill: "#999 0.6",
230 | risingStroke: "#999 0.6",
231 | fallingFill: "#999 0.6",
232 | fallingStroke: "#999 0.6"
233 | },
234 | selected: {
235 | risingStroke: a,
236 | fallingStroke: a,
237 | risingFill: a,
238 | fallingFill: a
239 | }
240 | },
241 | ohlc: {
242 | normal: { risingStroke: "#999 0.6", fallingStroke: "#999 0.6" },
243 | selected: { risingStroke: a, fallingStroke: a }
244 | }
245 | }
246 | }
247 | }
248 | };
249 | window.anychart = window.anychart || {};
250 | window.anychart.themes = window.anychart.themes || {};
251 | window.anychart.themes.darkGlamour = e;
252 | })();
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #303030
6 | #E82B70
7 | #202020
8 | #303030
9 | #ffffff
10 | #a2a2a2
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - Reply
5 | - Reply to all
6 |
7 |
8 |
9 | - reply
10 | - reply_all
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #008577
6 | #D81B60
7 | #ffffff
8 | #ffffff
9 | #000000
10 | #5d5d5d
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 50dp
3 | 8dp
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Phone Detector
3 | Show spam
4 | Show not spam
5 | Settings
6 | Statistics
7 | Undefined User
8 | Block number
9 | Add to Contacts
10 | Settings
11 |
12 | General
13 | Enable phone detection
14 | Access to the call log is required
15 |
16 | Disable search in Contacts
17 | Do not detect numbers from the contact list. Access to the contact list is required
18 | Detection timeout
19 | Set maximum search timeout for the incoming call (in seconds)
20 | Notifications
21 | Send block notification after a spam call
22 | \"Block\" notification
23 | Set \"block\" notification timeout (in minutes)
24 | Timeout
25 | Delete all log information
26 | Clear log
27 | Use only network data, ignore cached database
28 | Always use network
29 | Show information popup when incoming number is not detected
30 | Show empty phone popup
31 | Show notification instead of popup during the incoming call
32 | Call notification
33 | Do not save unrecognized numbers in the database
34 | Do not save unknown numbers
35 | Search Settings
36 | Use GetContact API for search
37 | GetContact
38 | Use neberitrubku.ru for search
39 | Neberitrubku.ru
40 | Access to the call permission is required
41 | Call on left swipe
42 | Theme
43 | Turn on dark mode
44 | Dark mode
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
15 |
16 |
17 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/root_preferences.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
12 |
13 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
38 |
43 |
44 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
66 |
67 |
71 |
72 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
96 |
101 |
102 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/app/src/test/java/com/leti/phonedetector/DBTest.kt:
--------------------------------------------------------------------------------
1 | package com.leti.phonedetector
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import androidx.test.core.app.ApplicationProvider
6 | import com.leti.phonedetector.database.PhoneLogDBHelper
7 | import com.leti.phonedetector.model.PhoneLogInfo
8 | import org.junit.Assert
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.RobolectricTestRunner
13 | import org.robolectric.annotation.Config
14 |
15 | @RunWith(RobolectricTestRunner::class)
16 | @Config(sdk = [Build.VERSION_CODES.P])
17 | class DBTest {
18 | private lateinit var db: PhoneLogDBHelper
19 |
20 | @Before
21 | fun fillDb() {
22 | val context = ApplicationProvider.getApplicationContext()
23 | db = PhoneLogDBHelper(context)
24 | db.fillSampleData()
25 | }
26 |
27 | @Test
28 | fun useAppContext() {
29 | // Context of the app under test.
30 | val context = ApplicationProvider.getApplicationContext()
31 | Assert.assertEquals("com.leti.phonedetector", context.packageName)
32 | }
33 |
34 | @Test
35 | fun testConnect() {
36 | val data = db.readPhoneLog()
37 | data.forEach { it ->
38 | Assert.assertNotNull(it.name)
39 | Assert.assertNotNull(it.number)
40 | Assert.assertNotNull(it.isSpam)
41 | Assert.assertNotNull(it.time)
42 | Assert.assertNotNull(it.date)
43 | }
44 | }
45 |
46 | @Test
47 | fun testClean() {
48 | db.cleanTables()
49 | Assert.assertEquals(0, db.readPhoneLog().count())
50 | db.fillSampleData()
51 | }
52 |
53 | @Test
54 | fun testInsert() {
55 | val phone = PhoneLogInfo(
56 | "Test123",
57 | "+79999999999",
58 | true,
59 | date = "2020.01.01",
60 | time = "20:01"
61 | )
62 | db.insertPhone(phone)
63 | val data = db.readPhoneLog()
64 | Assert.assertEquals(1, data.filter { it -> it.name == "Test123" }.count())
65 | }
66 |
67 | @Test
68 | fun testDelete() {
69 | db.deletePhoneInfo("+79999999999")
70 | val data = db.readPhoneLog()
71 | Assert.assertEquals(0, data.filter { it -> it.number == "+79999999999" }.count())
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/test/java/com/leti/phonedetector/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.leti.phonedetector
2 |
3 | import org.junit.Assert.*
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.50'
5 | repositories {
6 | google()
7 | jcenter()
8 |
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:7.1.2'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | maven { url 'https://jitpack.io' }
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/docs/UI.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UI.pdf
--------------------------------------------------------------------------------
/docs/UI_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UI_big.png
--------------------------------------------------------------------------------
/docs/UI_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UI_small.png
--------------------------------------------------------------------------------
/docs/UseCases.md:
--------------------------------------------------------------------------------
1 | # Сценарии использования приложения _caller-id_
2 |
3 | ## Идентификация во время входящего звонка
4 |
5 | Действующие лица: Пользователь приложения и лицо, совершающее вызов
6 |
7 | ### Основной сценрий
8 |
9 | 1. _Пользователь_ получает входящий звонок
10 |
11 | 2. _Пользователь_ получает **pop-up** уведомление с информацией о входящем номере
12 |
13 | - _Пользователю_ отобржается информация о входящем номере: имя и список ассоциированных хэш-тэгов
14 |
15 | - _Пользователю_ отобржается информация о вредоносности (спам) входящего номера
16 |
17 | ### Ошибка
18 |
19 | В случае отсутствия иноформации о входящем номере _Пользователю_ отображается сообщение об ошибке
20 |
21 | ## Получение напоминания о спам звонке
22 |
23 | Действующие лица: Пользователь приложения
24 |
25 | ### Основной сценрий
26 |
27 | 1. _Пользователь_ получает входящий звонок, который идентифицируется как спам
28 |
29 | 2. _Пользователь_ получает уведомление после полученного спам звонка
30 |
31 | ### Опциональные шаги
32 |
33 | 3. _Пользователь_ по нажатию на уведомление переходит в приложение
34 |
35 | 4. _Пользователю_ отображается информация о номере, с которого был совершен звонок
36 |
37 | 5. _Пользователь_ заносит номер в чёрный список (блокирует)
38 |
39 | ## Просмотр журнала звонков
40 |
41 | Действующее лицо: Пользователь приложения
42 |
43 | ### Основной сценарий
44 |
45 | 1. _Пользователь_ открывает приложение
46 |
47 | 2. _Пользовтелю_ отображается журнал входящих звонков: номер, время и дата звонка
48 |
49 | 3. _Пользователь_ настраивает отображение номеров
50 |
51 | - _Пользователь_ выполняет поиск по названию номера
52 |
53 | - _Пользователь_ включает/отключает отображение спам звонков
54 |
55 | - _Пользователь_ включает/отключает отображение **не** спам звонков
56 |
57 | ### Опциональные шаги
58 |
59 | 4. _Пользователь_ нажимает на запись о входящем звонке
60 |
61 | 5. _Пользователю_ отображается информация о звонке
62 |
63 | - _Пользователю_ отобржается информация о входящем номере: имя и список ассоциированных с ним хэш-тэгов
64 |
65 | - _Пользователю_ отобржается информация о вредоносности (спам) входяящего номера
66 |
67 | 6. _Пользователь_ совершает дополнительные действия с номером
68 |
69 | - В случае если номер **не** идентифицирован как вредоносный (спам) _Пользователь_ заносит номер в контакты
70 |
71 | - В случае если номер идентифицирован как вредоносный (спам) _Пользователь_ заносит номер в чёрный список (блокирует)
72 |
73 | ### Ошибка
74 |
75 | В случае отсутствия иноформации о входящем номере _Пользователю_ отображается сообщение об ошибке
76 |
77 | ## Настройка работы приложения
78 |
79 | Действующее лицо: Пользователь приложения
80 |
81 | 1. _Пользователь_ открывает приложение
82 |
83 | 2. _Пользователь_ выполняет настройку приложения
84 |
85 | - _Пользователь_ включает/отключает идентификацию номеров входящих звонков
86 |
87 | - _Пользователь_ включает/отключает поиск по контактам при идентификации номеров входящих звонков
88 |
89 | - _Пользователь_ настраивает допустимое время задержки (обращение к внешним API) во время идентификации номеров входящих звонков
90 |
91 | - _Пользователь_ настраивает включает/отключает напоминания о спам звонках
92 |
93 | - _Пользователь_ устанавливает задержку при отправке напоминания о спам звонке, если такие напоминания включены
94 |
--------------------------------------------------------------------------------
/docs/UseCases/analog/ad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/ad.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/contact_from_logs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/contact_from_logs/1.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/contact_from_logs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/contact_from_logs/2.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/contact_from_logs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/contact_from_logs/3.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/contact_from_logs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/contact_from_logs/4.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/identification/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/identification/1.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/identification/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/identification/2.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/identification/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/identification/3.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/notification/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/notification/1.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/notification/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/notification/2.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/notification/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/notification/3.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/notification/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/notification/4.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/1.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/2.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/3.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/4.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/5.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/6.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/7.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/8.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/settings/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/settings/9.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/spam_from_logs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/spam_from_logs/1.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/spam_from_logs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/spam_from_logs/2.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/spam_from_logs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/spam_from_logs/3.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/spam_from_logs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/spam_from_logs/4.png
--------------------------------------------------------------------------------
/docs/UseCases/analog/spam_from_logs/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/analog/spam_from_logs/5.png
--------------------------------------------------------------------------------
/docs/UseCases/contact_from_logs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/contact_from_logs/1.png
--------------------------------------------------------------------------------
/docs/UseCases/contact_from_logs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/contact_from_logs/2.png
--------------------------------------------------------------------------------
/docs/UseCases/contact_from_logs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/contact_from_logs/3.png
--------------------------------------------------------------------------------
/docs/UseCases/contact_from_logs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/contact_from_logs/4.png
--------------------------------------------------------------------------------
/docs/UseCases/identification/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/identification/1.png
--------------------------------------------------------------------------------
/docs/UseCases/identification/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/identification/2.png
--------------------------------------------------------------------------------
/docs/UseCases/identification/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/identification/3.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/1.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/2.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/3.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/4.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/5.png
--------------------------------------------------------------------------------
/docs/UseCases/notification/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/notification/6.png
--------------------------------------------------------------------------------
/docs/UseCases/possible_improve/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/possible_improve/1.png
--------------------------------------------------------------------------------
/docs/UseCases/possible_improve/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/possible_improve/2.png
--------------------------------------------------------------------------------
/docs/UseCases/possible_improve/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/possible_improve/3.png
--------------------------------------------------------------------------------
/docs/UseCases/settings/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/settings/1.png
--------------------------------------------------------------------------------
/docs/UseCases/settings/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/settings/2.png
--------------------------------------------------------------------------------
/docs/UseCases/settings/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/settings/3.png
--------------------------------------------------------------------------------
/docs/UseCases/settings/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/settings/4.png
--------------------------------------------------------------------------------
/docs/UseCases/settings/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/settings/5.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/1.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/2.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/3.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/4.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/5.png
--------------------------------------------------------------------------------
/docs/UseCases/spam_from_logs/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/UseCases/spam_from_logs/6.png
--------------------------------------------------------------------------------
/docs/db/db.drawio:
--------------------------------------------------------------------------------
1 | 7VxZk9o4EP41VO0+MOWT43FgjiQ7u0VCNsk+TQlbGGVsi7XFAPn1advyKWObCZgJA8WD1W61292fpO6WoKOOnc29h5aLv6mJ7Y4imZuOetNRFFlTlE7wlcxtROlrekSwPGJyppQwJT8wJ0qcuiIm9nOMjFKbkWWeaFDXxQbL0ZDn0XWebU7t/FOXyMICYWogW6R+JSZbRNSB0k/p7zCxFvGT5d4wuuOgmJm/ib9AJl1nSOptRx17lLLoytmMsR0YL7bL1/fbr/bDU+/+w0f/f/Tv6K/P/3zpRsLu9umSvIKHXfZi0T+e5nfvvmjfvy0ndx/X0v317EOXd5Gekb3i9posqIvfu3N66zJvC/f+IND4k5uAbWO7+mvi2MiF1mhOXTbldyRoI5tYLlwboC72gPCMPUbAJdf8BqNLoBoLYpsPaEtXwUv5DBlPcWu0oB75AWKRDbdkIMBtj3F0Kb0cxzToyR/tYR94JrGl5IT0gHzGeQxq22jpk1mocMDiIM8i7ogyRp1YEF25JjZ5K3F92GAefUrAFPRv6B/ux8AaeJNBJ/fXPaYOjmzO7+oS9w8fe3K/F7XXKZLlAedZZFGscSLio8dKZCeP+wSjDbkWGCF5nqIWnheLyT6vV/I8pZd/HLLB8S5ieBSY0c/iEi4yr5qSQrTugVxZQK6LHLi6BuKUecS1BMiC1VkGnjaes53g9JfIABkPIc+NllI+8bcOSBT6zu0QGAtimtgNgcMQQ7NkMCwpcVloFn0EXzDeWLrSOzooNIa2nLbhG7B7bExdwBgiIZgwAHeNA/CWwKxyTNfDbJv3Xh2qil7Ogirn3n19qYi+XDmzYOo4Y29WzDAL5tj8Mju36scCgK6cGACqAADiT5fIiQAwgngBI/eNIuBYTu8P2nN6d6qhm8nM2DwMt5v+0/cJWftlTnfCYO7ND/odrmyKgp0ulyXtxANd7pcHnA/USuJNm1qXcPMU4aZSCDd7JXGBXhZtxrDaL9rU9d8o2hw0ClFAMVVOR/AZTVwviUOSwf5LkWgZ4o43Pw0FPzNy5mnF8QLR/QFQFom2CgBFrIiYMLNcANASAMqi0nYB0BMAIDgcu+Z1UJqEFl2G3qlZsSPKmNrUCyWoUvgRbA+C74gdByHYtHBscWzP6Po2JYzwhrBvcTe4/i+4BodHrZtN5tbNNhEPFvoWSw8amV5BM+0WtuJ++wUcPl15Bq4yccQHIZaFK9HDsRDYoUn8ErxINqDQuEIethEjz/mCcEWsMgmGVipWVgpxiqznRUQvzHtlK7AFQcX4SqjSRRapF9TV8oJUJS+Hzuc+zokAuKJthoPPHjvfWesVVO1LlRpp/QK/NKzkVwd5/mGN+IILBtXsSiGy1KrZ5WFBerXuSvFd67SR8/xKjTb7Ka8WsVAjvWBJPccOFxFSDho8K2LSF08V6Zwa5k2Z9a1mNZ3x7Glkoxm2R5CSWeFUGs+xLk1zxSiNk5XmxZR4EeCbUlytTrIV1Dyn6h5mDuoWMBTPobtH/MGcJ2Y+xJ03dp8XLdm/rf8O4z61Be+V1tg0wXlxueUzsvyk5MKg0bjmor3dmktNSW53VNR4i08tK4KUlegGUi0If58dvlLs6gJ2eckFRvE5p2MliVYN7nYP/Vex51eqnphmwSQkuPZ8C2ovTroPj4U2t/9K1ROLboK7X1PK3ZWuJFnO5N1AkIdKs9Q7aTRLvQMhE+wRMHSwzmbS8UJWfMD8nG/MZfPz3RN029m4VkjWkhVt32xcVWsEveJsXK9O8oo2qstQNb1oCq3adAV9FF4R2Sm/EOQoUjW/EKX0qvnlflGfav2L9QG1Rp9igUDWavj31F8tVpj0GvnCubAc/y9n8aWjPc5AX00W3zi5a7BADiunsiMl8XLvajismysO5j7xwJx0FU/Rp0rk23PhgTw4BI9JmY+Sc2h7eb2sCN78DOGOm2b0Qat5Tv+Gz1EcPqcX6vyDptva6jkdpCgHrnjqC2H/8Qlvzzqnf/m0Fo30V3Fiolw/scSIXNOjxHyk/ln79IBn+V7i+TaPSpTrJxboTPxMDPwYdLtUcVrAQJunJcr1E8t4xH9ERhRj5Q5vX0DQTrQR76WfEBTifu/SI88QV6Tr/HteVLng4Fg4UOWThwbi1rGHHbDbowGWCtKqCxZawoJ28mCh5GBtkKJ2znwz71jbPIcARe/U0UPJYVuIHmCtgMx++7jycYKOy0/AWkHEsOT3QQdCBDTTn8pH1YT0DwfU258=
--------------------------------------------------------------------------------
/docs/db/db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/db/db.png
--------------------------------------------------------------------------------
/docs/Пояснительная_записка.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/Пояснительная_записка.docx
--------------------------------------------------------------------------------
/docs/Пояснительная_записка.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/docs/Пояснительная_записка.pdf
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/play_market_publication/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/play_market_publication/icon.png
--------------------------------------------------------------------------------
/play_market_publication/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/play_market_publication/screenshot_1.png
--------------------------------------------------------------------------------
/play_market_publication/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/play_market_publication/screenshot_2.png
--------------------------------------------------------------------------------
/play_market_publication/screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kovinevmv/PhoneDetector/fe575655c8ee161ab8dadaedf7a39c2740ab8d1e/play_market_publication/screenshot_3.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name='PhoneDetector'
3 |
--------------------------------------------------------------------------------