├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── .idea
├── .gitignore
├── assetWizardSettings.xml
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── deploymentTargetSelector.xml
├── encodings.xml
├── gradle.xml
├── jarRepositories.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── modules.xml
├── navEditor.xml
├── other.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── schemas
│ └── li.doerf.hacked.db.AppDatabase
│ │ ├── 4.json
│ │ ├── 5.json
│ │ ├── 6.json
│ │ ├── 7.json
│ │ └── 8.json
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── li
│ │ └── doerf
│ │ └── hacked
│ │ ├── HackedApplication.java
│ │ ├── activities
│ │ └── SettingsActivity.java
│ │ ├── db
│ │ ├── AppDatabase.java
│ │ ├── daos
│ │ │ ├── AccountDao.java
│ │ │ ├── BreachDao.java
│ │ │ └── BreachedSiteDao.java
│ │ └── entities
│ │ │ ├── Account.java
│ │ │ ├── Breach.java
│ │ │ └── BreachedSite.java
│ │ ├── services
│ │ └── FirebaseMessagagingService.java
│ │ ├── ui
│ │ ├── HibpInfo.java
│ │ ├── fragments
│ │ │ └── SettingsFragment.java
│ │ └── viewmodels
│ │ │ ├── AccountViewModel.java
│ │ │ └── BreachViewModel.java
│ │ └── utils
│ │ ├── AccountHelper.java
│ │ ├── NotificationHelper.java
│ │ ├── OreoNotificationHelper.java
│ │ ├── StringHelper.java
│ │ └── SynchronizationHelper.java
│ ├── kotlin
│ └── li
│ │ └── doerf
│ │ └── hacked
│ │ ├── CustomEvent.kt
│ │ ├── activities
│ │ └── NavActivity.kt
│ │ ├── initializer
│ │ ├── BackgroundSyncInitializer.kt
│ │ └── FirebaseAnalyticsInitializer.kt
│ │ ├── remote
│ │ ├── hibp
│ │ │ ├── BreachedAccount.kt
│ │ │ ├── BreachedSitesWorker.kt
│ │ │ ├── HIBPAccountCheckerWorker.kt
│ │ │ ├── HIBPAccountResponseWorker.kt
│ │ │ └── WorkFailedException.kt
│ │ └── pwnedpasswords
│ │ │ └── PwnedPassword.kt
│ │ ├── services
│ │ └── AccountService.kt
│ │ ├── ui
│ │ ├── RateUsDialogFragment.kt
│ │ ├── adapters
│ │ │ ├── AccountsAdapter.kt
│ │ │ ├── BreachedSitesAdapter.kt
│ │ │ └── RecyclerViewHolder.kt
│ │ ├── composable
│ │ │ └── Breach.kt
│ │ ├── fragments
│ │ │ ├── AccountDetailsFragment.kt
│ │ │ ├── AccountsFragment.kt
│ │ │ ├── AllBreachesFragment.kt
│ │ │ ├── BreachesFragment.kt
│ │ │ ├── FirstUseFragment.kt
│ │ │ ├── OverviewFragment.kt
│ │ │ └── PwnedPasswordFragment.kt
│ │ └── viewmodels
│ │ │ └── BreachedSitesViewModel.kt
│ │ └── util
│ │ ├── Analytics.kt
│ │ ├── AppReview.kt
│ │ ├── CoroutineHelper.kt
│ │ ├── FcmTokenManager.kt
│ │ ├── LogHelper.kt
│ │ ├── NavEvent.kt
│ │ └── RatingHelper.kt
│ └── res
│ ├── anim
│ └── slide_out.xml
│ ├── drawable
│ ├── chevron_right_black.xml
│ ├── ic_add.xml
│ ├── ic_arrow_drop_down.xml
│ ├── ic_arrow_drop_up.xml
│ ├── ic_refresh.xml
│ ├── section_header.xml
│ └── side_nav_bar.xml
│ ├── layout
│ ├── activity_nav.xml
│ ├── activity_settings.xml
│ ├── card_account.xml
│ ├── card_breach.xml
│ ├── card_breached_site.xml
│ ├── card_breached_site_compact.xml
│ ├── fragment_account_details.xml
│ ├── fragment_accounts.xml
│ ├── fragment_all_breaches.xml
│ ├── fragment_breaches.xml
│ ├── fragment_first_use.xml
│ ├── fragment_overview.xml
│ ├── fragment_pwned_password.xml
│ └── hibp_info.xml
│ ├── menu
│ ├── menu_fragment_account_details.xml
│ ├── menu_fragment_accounts.xml
│ ├── menu_fragment_allbreaches.xml
│ └── menu_nav.xml
│ ├── mipmap-anydpi-v26
│ ├── app_icon.xml
│ └── app_icon_round.xml
│ ├── mipmap-hdpi
│ ├── app_icon.png
│ ├── app_icon_foreground.png
│ └── app_icon_round.png
│ ├── mipmap-mdpi
│ ├── app_icon.png
│ ├── app_icon_foreground.png
│ └── app_icon_round.png
│ ├── mipmap-xhdpi
│ ├── app_icon.png
│ ├── app_icon_foreground.png
│ └── app_icon_round.png
│ ├── mipmap-xxhdpi
│ ├── app_icon.png
│ ├── app_icon_foreground.png
│ └── app_icon_round.png
│ ├── mipmap-xxxhdpi
│ ├── app_icon.png
│ ├── app_icon_foreground.png
│ └── app_icon_round.png
│ ├── navigation
│ └── nav_graph.xml
│ ├── values-de
│ ├── arrays.xml
│ └── strings.xml
│ ├── values-fr
│ └── strings.xml
│ ├── values-v21
│ └── styles.xml
│ ├── values-w820dp
│ └── dimens.xml
│ ├── values
│ ├── app_icon_background.xml
│ ├── arrays.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── drawables.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ ├── backup_descriptor.xml
│ ├── global_tracker.xml
│ └── preferences.xml
├── build.gradle
├── docs
├── _config.yml
└── privacy.md
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── startEmulator.sh
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2.3.4
12 | - name: set up JDK 17
13 | uses: actions/setup-java@v1
14 | with:
15 | java-version: 17
16 | - name: Decode google-services.json
17 | env:
18 | FIREBASE_CONFIG: ${{ secrets.FIREBASE_CONFIG }}
19 | run: mkdir -p app/src/debug && echo $FIREBASE_CONFIG > app/src/debug/google-services.json
20 | - name: Build with Gradle
21 | run: ./gradlew dependencies assembleDebug
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | /.idea/caches/build_file_checksums.ser
10 | /.idea/caches/gradle_models.ser
11 | .project
12 | .settings/
13 | google-services.json
14 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Default ignored files
3 | /shelf/
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | xmlns:android
14 |
15 | ^$
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | xmlns:.*
25 |
26 | ^$
27 |
28 |
29 | BY_NAME
30 |
31 |
32 |
33 |
34 |
35 |
36 | .*:id
37 |
38 | http://schemas.android.com/apk/res/android
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | .*:name
48 |
49 | http://schemas.android.com/apk/res/android
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | name
59 |
60 | ^$
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | style
70 |
71 | ^$
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | .*
81 |
82 | ^$
83 |
84 |
85 | BY_NAME
86 |
87 |
88 |
89 |
90 |
91 |
92 | .*
93 |
94 | http://schemas.android.com/apk/res/android
95 |
96 |
97 | ANDROID_ATTRIBUTE_ORDER
98 |
99 |
100 |
101 |
102 |
103 |
104 | .*
105 |
106 | .*
107 |
108 |
109 | BY_NAME
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/navEditor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Hacked?
4 |
5 | Android app to track accounts on http://www.haveibeenpwned.com
6 |
7 | App can be downloaded from the Google Play Store at https://play.google.com/store/apps/details?id=li.doerf.hacked
8 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.google.gms.google-services'
4 | apply plugin: 'com.google.firebase.crashlytics'
5 | apply plugin: "androidx.navigation.safeargs"
6 |
7 | android {
8 | compileSdk 35
9 | buildToolsVersion = '35.0.0'
10 |
11 | defaultConfig {
12 | applicationId "li.doerf.hacked"
13 | minSdkVersion 23
14 | targetSdkVersion 34
15 | versionCode 118
16 | versionName "3.7.8"
17 | multiDexEnabled true
18 | javaCompileOptions {
19 | annotationProcessorOptions {
20 | arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
21 | }
22 | }
23 |
24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
25 | }
26 | buildTypes {
27 | debug {
28 | applicationIdSuffix '.debug'
29 | versionNameSuffix '-dbg'
30 | }
31 | release {
32 | minifyEnabled false
33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
34 | }
35 | }
36 | buildFeatures {
37 | viewBinding true
38 | compose true
39 | }
40 | compileOptions {
41 | sourceCompatibility = JavaVersion.VERSION_1_8
42 | targetCompatibility = JavaVersion.VERSION_1_8
43 | }
44 | kotlinOptions {
45 | jvmTarget = JavaVersion.VERSION_1_8.toString()
46 | }
47 | sourceSets {
48 | main.java.srcDirs += 'src/main/kotlin'
49 | }
50 | composeOptions {
51 | kotlinCompilerExtensionVersion = compose_compiler_version
52 | }
53 | namespace 'li.doerf.hacked'
54 | }
55 |
56 | dependencies {
57 | implementation "androidx.core:core-ktx:$androidx_core_version"
58 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
59 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
60 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version"
61 |
62 | implementation 'androidx.multidex:multidex:2.0.1'
63 | implementation 'androidx.appcompat:appcompat:1.7.0'
64 | implementation 'androidx.cardview:cardview:1.0.0'
65 | implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
66 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
67 | implementation "com.google.android.material:material:$material_version"
68 | implementation("androidx.work:work-runtime:$work_version")
69 | implementation("androidx.work:work-runtime-ktx:$work_version")
70 | implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
71 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
72 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
73 | implementation "com.google.android.play:review:$play_review_version"
74 | implementation "com.google.android.play:review-ktx:$play_review_version"
75 | implementation "com.google.android.gms:play-services-base:$play_services_base_version"
76 |
77 | // jetpack compose
78 | implementation("androidx.compose.compiler:compiler:$compose_compiler_version")
79 | implementation "androidx.activity:activity-compose:$activity_compose"
80 | implementation "androidx.compose.material:material:$compose_version"
81 | implementation "androidx.compose.animation:animation:$compose_version"
82 | implementation "androidx.compose.ui:ui-tooling:$compose_version"
83 | // compose theme compatibility
84 | implementation "com.google.accompanist:accompanist-appcompat-theme:$accompanist_appcompat_theme"
85 | implementation "com.google.accompanist:accompanist-coil:$accompanist_coil"
86 |
87 | implementation "androidx.preference:preference-ktx:$androidx_preference_version"
88 | implementation "androidx.startup:startup-runtime:$startup_version"
89 |
90 | implementation "androidx.room:room-runtime:$room_version"
91 | implementation "androidx.room:room-rxjava2:$room_version"
92 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
93 | annotationProcessor "androidx.room:room-compiler:$room_version"
94 |
95 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
96 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
97 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
98 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
99 |
100 | implementation(platform("com.google.firebase:firebase-bom:$firebase_bom_version"))
101 | implementation 'com.google.firebase:firebase-analytics'
102 | implementation 'com.google.firebase:firebase-messaging'
103 | implementation 'com.google.firebase:firebase-crashlytics'
104 | implementation 'com.google.firebase:firebase-analytics'
105 |
106 | implementation "joda-time:joda-time:$jodatime_version"
107 | implementation 'commons-codec:commons-codec:1.17.0'
108 | implementation "com.google.code.gson:gson:$gson_version"
109 | implementation "com.squareup.picasso:picasso:$picasso_version"
110 |
111 | implementation "com.github.kittinunf.fuel:fuel:$fuel_version"
112 | implementation "com.github.kittinunf.fuel:fuel-coroutines:$fuel_version"
113 | implementation "com.github.kittinunf.fuel:fuel-jackson:$fuel_version"
114 | implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
115 |
116 | androidTestImplementation 'androidx.test:runner:1.5.2'
117 | androidTestImplementation 'androidx.test:rules:1.5.0'
118 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
119 |
120 | androidTestImplementation "androidx.work:work-testing:$work_version"
121 | }
122 |
123 | repositories {
124 | mavenCentral()
125 | }
126 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/moo/Development/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
42 |
43 |
44 |
45 |
46 |
47 |
52 |
53 |
55 |
57 |
58 |
59 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/HackedApplication.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked;
2 |
3 | import android.os.Bundle;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.lifecycle.DefaultLifecycleObserver;
8 | import androidx.lifecycle.LifecycleObserver;
9 | import androidx.lifecycle.LifecycleOwner;
10 | import androidx.lifecycle.ProcessLifecycleOwner;
11 | import androidx.multidex.MultiDexApplication;
12 |
13 | import com.google.firebase.analytics.FirebaseAnalytics;
14 |
15 | import io.reactivex.processors.PublishProcessor;
16 | import li.doerf.hacked.util.Analytics;
17 | import li.doerf.hacked.util.NavEvent;
18 |
19 | /**
20 | * Created by moo on 25.05.17.
21 | */
22 |
23 | public class HackedApplication extends MultiDexApplication implements LifecycleObserver, DefaultLifecycleObserver {
24 | private static final String TAG = "HackedApplication";
25 | private final PublishProcessor navEvents = PublishProcessor.create();
26 |
27 | @Override
28 | public void onCreate() {
29 | super.onCreate();
30 | ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
31 | }
32 |
33 | @Override
34 | public void onStart(@NonNull LifecycleOwner owner) {
35 | Log.d(TAG, "application opened");
36 | Analytics.Companion.getInstance().logEvent(FirebaseAnalytics.Event.APP_OPEN, new Bundle());
37 | }
38 |
39 | public PublishProcessor getNavEvents() {
40 | return navEvents;
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/activities/SettingsActivity.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.activities;
2 |
3 | import android.os.Bundle;
4 |
5 | import androidx.appcompat.app.AppCompatActivity;
6 | import androidx.appcompat.widget.Toolbar;
7 |
8 | import li.doerf.hacked.R;
9 |
10 | /**
11 | * Created by moo on 01/12/15.
12 | */
13 | public class SettingsActivity extends AppCompatActivity {
14 | // private final String LOGTAG = getClass().getSimpleName();
15 |
16 | @Override
17 | public void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | setContentView(R.layout.activity_settings);
20 |
21 | Toolbar toolbar = findViewById(R.id.toolbar);
22 | setSupportActionBar(toolbar);
23 | toolbar.setTitle(R.string.action_settings);
24 | setTitle(R.string.action_settings);
25 |
26 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/AppDatabase.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.room.Database;
6 | import androidx.room.Room;
7 | import androidx.room.RoomDatabase;
8 | import androidx.room.migration.Migration;
9 | import androidx.sqlite.db.SupportSQLiteDatabase;
10 |
11 | import li.doerf.hacked.db.daos.AccountDao;
12 | import li.doerf.hacked.db.daos.BreachDao;
13 | import li.doerf.hacked.db.daos.BreachedSiteDao;
14 | import li.doerf.hacked.db.entities.Account;
15 | import li.doerf.hacked.db.entities.Breach;
16 | import li.doerf.hacked.db.entities.BreachedSite;
17 |
18 | @Database(entities = {Account.class, Breach.class, BreachedSite.class}, version = 8)
19 | public abstract class AppDatabase extends RoomDatabase {
20 |
21 | private static AppDatabase instance;
22 |
23 | private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
24 | @Override
25 | public void migrate(
26 | SupportSQLiteDatabase database) {
27 | // Since we didn’t alter the table, there’s nothing else
28 | // to do here.
29 | }
30 | };
31 |
32 | private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
33 | @Override
34 | public void migrate(
35 | SupportSQLiteDatabase database) {
36 | database.execSQL("ALTER TABLE `accounts` ADD COLUMN `num_breaches` INTEGER;");
37 | database.execSQL("ALTER TABLE `accounts` ADD COLUMN `num_acknowledged_breaches` INTEGER;");
38 | }
39 | };
40 |
41 | private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
42 | @Override
43 | public void migrate(
44 | SupportSQLiteDatabase database) {
45 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `is_sensitive` INTEGER DEFAULT 0;");
46 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `is_retired` INTEGER DEFAULT 0;");
47 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `is_fabricated` INTEGER DEFAULT 0;");
48 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `is_spam_list` INTEGER DEFAULT 0;");
49 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `is_sensitive` INTEGER DEFAULT 0;");
50 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `is_retired` INTEGER DEFAULT 0;");
51 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `is_fabricated` INTEGER DEFAULT 0;");
52 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `is_spam_list` INTEGER DEFAULT 0;");
53 | }
54 | };
55 |
56 | private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
57 | @Override
58 | public void migrate(
59 | SupportSQLiteDatabase database) {
60 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `logo_path` TEXT DEFAULT null;");
61 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `logo_path` TEXT DEFAULT null;");
62 | }
63 | };
64 |
65 | private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
66 | @Override
67 | public void migrate(
68 | SupportSQLiteDatabase database) {
69 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `modified_date` INTEGER DEFAULT 0;");
70 | database.execSQL("ALTER TABLE `breaches` ADD COLUMN `last_checked` INTEGER DEFAULT 0;");
71 | database.execSQL("ALTER TABLE `breached_sites` ADD COLUMN `modified_date` INTEGER DEFAULT 0;");
72 | }
73 | };
74 |
75 | public static AppDatabase get(Context context) {
76 | synchronized (AppDatabase.class) {
77 | if (instance != null) {
78 | return instance;
79 | }
80 |
81 | instance = Room.databaseBuilder(context, AppDatabase.class, "hacked.db")
82 | .addMigrations(
83 | MIGRATION_3_4,
84 | MIGRATION_4_5,
85 | MIGRATION_5_6,
86 | MIGRATION_6_7,
87 | MIGRATION_7_8)
88 | .build();
89 | return instance;
90 | }
91 | }
92 |
93 | public abstract AccountDao getAccountDao();
94 |
95 | public abstract BreachDao getBreachDao();
96 |
97 | public abstract BreachedSiteDao getBrachedSiteDao();
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/daos/AccountDao.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.daos;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import androidx.room.Dao;
5 | import androidx.room.Delete;
6 | import androidx.room.Insert;
7 | import androidx.room.Query;
8 | import androidx.room.Update;
9 |
10 | import java.util.List;
11 |
12 | import li.doerf.hacked.db.entities.Account;
13 |
14 | @Dao
15 | public interface AccountDao {
16 |
17 | @Insert
18 | List insert(Account... entities);
19 |
20 | @Update
21 | void update(Account entity);
22 |
23 | @Delete
24 | int delete(Account entity);
25 |
26 | @Query("SELECT * FROM accounts ORDER BY name")
27 | List getAll();
28 |
29 | @Query("SELECT * FROM accounts ORDER BY is_hacked DESC, name")
30 | LiveData> getAllLD();
31 |
32 | @Query("SELECT * FROM accounts where _id=:anId")
33 | List findById(Long anId);
34 |
35 | @Query("SELECT * FROM accounts where name=:name")
36 | List findByName(String name);
37 |
38 | @Query("SELECT count(*) FROM accounts where name=:aName")
39 | Integer countByName(String aName);
40 |
41 | @Query("SELECT * FROM accounts ORDER BY last_checked DESC LIMIT 1")
42 | LiveData getLastChecked();
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/daos/BreachDao.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.daos;
2 |
3 | import java.util.List;
4 |
5 | import androidx.lifecycle.LiveData;
6 | import androidx.room.Dao;
7 | import androidx.room.Delete;
8 | import androidx.room.Insert;
9 | import androidx.room.Query;
10 | import androidx.room.Update;
11 | import li.doerf.hacked.db.entities.Breach;
12 |
13 | @Dao
14 | public interface BreachDao {
15 | @Insert
16 | List insert(Breach... entities);
17 |
18 | @Update
19 | int update(Breach entity);
20 |
21 | @Delete
22 | int delete(Breach entity);
23 |
24 | @Query("SELECT COUNT(*) FROM breaches WHERE account=:account and is_acknowledged=0")
25 | Long countUnacknowledged(Long account);
26 |
27 | @Query("SELECT * FROM breaches WHERE _id=:id")
28 | Breach findById(Long id);
29 |
30 | @Query("SELECT * FROM breaches WHERE account=:accountId ORDER BY is_acknowledged, breach_date DESC")
31 | List findByAccount(Long accountId);
32 |
33 | @Query("SELECT * FROM breaches WHERE account=:accountId ORDER BY is_acknowledged, breach_date DESC")
34 | LiveData> findByAccountLD(Long accountId);
35 |
36 | @Query("SELECT * FROM breaches WHERE account=:accountId AND name=:aName ORDER BY name")
37 | Breach findByAccountAndName(Long accountId, String aName);
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/daos/BreachedSiteDao.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.daos;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import androidx.room.Dao;
5 | import androidx.room.Delete;
6 | import androidx.room.Insert;
7 | import androidx.room.Query;
8 | import androidx.room.Update;
9 |
10 | import java.util.List;
11 |
12 | import li.doerf.hacked.db.entities.BreachedSite;
13 |
14 | @Dao
15 | public interface BreachedSiteDao {
16 |
17 | @Insert
18 | List insert(BreachedSite... entities);
19 |
20 | @Update
21 | int update(BreachedSite entity);
22 |
23 | @Delete
24 | int delete(BreachedSite... entity);
25 |
26 | @Query("SELECT * FROM breached_sites WHERE name=:name LIMIT 1")
27 | BreachedSite getByName(String name);
28 |
29 | @Query("SELECT * FROM breached_sites ORDER BY name")
30 | List getAll();
31 |
32 | @Query("SELECT * FROM breached_sites ORDER BY name")
33 | LiveData> getAllLD();
34 |
35 | @Query("SELECT * FROM breached_sites ORDER BY pwn_count DESC")
36 | LiveData> getAllByPwnCountLD();
37 |
38 | @Query("SELECT * FROM breached_sites ORDER BY added_date DESC")
39 | LiveData> getAllByDateAddedLD();
40 |
41 | @Query("SELECT * FROM breached_sites WHERE name LIKE :name ORDER BY name")
42 | LiveData> getAllByName(String name);
43 |
44 | @Query("SELECT * FROM breached_sites ORDER BY pwn_count DESC LIMIT 20")
45 | LiveData> listTop20();
46 |
47 | @Query("SELECT * FROM breached_sites ORDER BY added_date DESC LIMIT 3")
48 | LiveData> listMostRecent();
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/entities/Account.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.entities;
2 |
3 | import org.joda.time.DateTime;
4 |
5 | import androidx.room.ColumnInfo;
6 | import androidx.room.Entity;
7 | import androidx.room.PrimaryKey;
8 |
9 | @Entity(tableName = "accounts")
10 | public class Account {
11 |
12 | @PrimaryKey
13 | @ColumnInfo(name="_id")
14 | private Long id;
15 | @ColumnInfo(name="name")
16 | private String name;
17 | @ColumnInfo(name="last_checked")
18 | private Long lastChecked;
19 | @ColumnInfo(name="is_hacked")
20 | private Boolean isHacked = false;
21 | @ColumnInfo(name="num_breaches")
22 | private Integer numBreaches;
23 | @ColumnInfo(name="num_acknowledged_breaches")
24 | private Integer numAcknowledgedBreaches;
25 |
26 |
27 | public Long getId() {
28 | return id;
29 | }
30 |
31 | public void setId(Long id) {
32 | this.id = id;
33 | }
34 |
35 | public String getName() {
36 | return name;
37 | }
38 |
39 | public void setName(String name) {
40 | this.name = name;
41 | }
42 |
43 | public Long getLastChecked() {
44 | return lastChecked;
45 | }
46 |
47 | public void setLastChecked(DateTime lastChecked) {
48 | this.lastChecked = lastChecked != null ? lastChecked.getMillis() : null;
49 | }
50 | public void setLastChecked(Long lastChecked) {
51 | this.lastChecked = lastChecked;
52 | }
53 |
54 | public Boolean getHacked() {
55 | return isHacked;
56 | }
57 |
58 | public void setHacked(Boolean hacked) {
59 | isHacked = hacked;
60 | }
61 |
62 | public Integer getNumBreaches() {
63 | return numBreaches;
64 | }
65 |
66 | public void setNumBreaches(Integer numBreaches) {
67 | this.numBreaches = numBreaches;
68 | }
69 |
70 | public Integer getNumAcknowledgedBreaches() {
71 | return numAcknowledgedBreaches;
72 | }
73 |
74 | public void setNumAcknowledgedBreaches(Integer numAcknowledgedBreaches) {
75 | this.numAcknowledgedBreaches = numAcknowledgedBreaches;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/entities/Breach.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.entities;
2 |
3 | import androidx.room.ColumnInfo;
4 | import androidx.room.Entity;
5 | import androidx.room.PrimaryKey;
6 |
7 | @Entity(tableName = "breaches")
8 | public class Breach {
9 | @PrimaryKey
10 | @ColumnInfo(name="_id")
11 | private Long id;
12 | @ColumnInfo(name = "account")
13 | private Long account;
14 | @ColumnInfo(name = "name")
15 | private String name;
16 | @ColumnInfo(name = "title")
17 | private String title;
18 | @ColumnInfo(name = "domain")
19 | private String domain;
20 | @ColumnInfo(name = "breach_date")
21 | private Long breachDate;
22 | @ColumnInfo(name = "added_date")
23 | private Long addedDate;
24 | @ColumnInfo(name = "modified_date")
25 | private Long modifiedDate;
26 | @ColumnInfo(name = "pwn_count")
27 | private Long pwnCount;
28 | @ColumnInfo(name = "description")
29 | private String description;
30 | @ColumnInfo(name = "data_classes")
31 | private String dataClasses;
32 | @ColumnInfo(name = "is_verified")
33 | private Boolean isVerified;
34 | @ColumnInfo(name = "is_acknowledged")
35 | private Boolean isAcknowledged;
36 | @ColumnInfo(name = "is_sensitive")
37 | private Boolean isSensitive;
38 | @ColumnInfo(name = "is_retired")
39 | private Boolean isRetired;
40 | @ColumnInfo(name = "is_fabricated")
41 | private Boolean isFabricated;
42 | @ColumnInfo(name = "is_spam_list")
43 | private Boolean isSpamList;
44 | @ColumnInfo(name = "logo_path")
45 | private String logoPath;
46 | @ColumnInfo(name = "last_checked")
47 | private Long lastChecked;
48 |
49 | public Long getId() {
50 | return id;
51 | }
52 |
53 | public void setId(Long id) {
54 | this.id = id;
55 | }
56 |
57 | public Long getAccount() {
58 | return account;
59 | }
60 |
61 | public void setAccount(Long account) {
62 | this.account = account;
63 | }
64 |
65 | public String getName() {
66 | return name;
67 | }
68 |
69 | public void setName(String name) {
70 | this.name = name;
71 | }
72 |
73 | public String getTitle() {
74 | return title;
75 | }
76 |
77 | public void setTitle(String title) {
78 | this.title = title;
79 | }
80 |
81 | public String getDomain() {
82 | return domain;
83 | }
84 |
85 | public void setDomain(String domain) {
86 | this.domain = domain;
87 | }
88 |
89 | public Long getBreachDate() {
90 | return breachDate;
91 | }
92 |
93 | public void setBreachDate(Long breachDate) {
94 | this.breachDate = breachDate;
95 | }
96 |
97 | public Long getAddedDate() {
98 | return addedDate;
99 | }
100 |
101 | public void setAddedDate(Long addedDate) {
102 | this.addedDate = addedDate;
103 | }
104 |
105 | public Long getModifiedDate() {
106 | return modifiedDate;
107 | }
108 |
109 | public void setModifiedDate(Long modifiedDate) {
110 | this.modifiedDate = modifiedDate;
111 | }
112 |
113 | public Long getPwnCount() {
114 | return pwnCount;
115 | }
116 |
117 | public void setPwnCount(Long pwnCount) {
118 | this.pwnCount = pwnCount;
119 | }
120 |
121 | public String getDescription() {
122 | return description;
123 | }
124 |
125 | public void setDescription(String description) {
126 | this.description = description;
127 | }
128 |
129 | public String getDataClasses() {
130 | return dataClasses;
131 | }
132 |
133 | public void setDataClasses(String dataClasses) {
134 | this.dataClasses = dataClasses;
135 | }
136 |
137 | public Boolean getVerified() {
138 | if (isVerified == null) {
139 | return false;
140 | }
141 | return isVerified;
142 | }
143 |
144 | public void setVerified(Boolean verified) {
145 | isVerified = verified;
146 | }
147 |
148 | public Boolean getAcknowledged() {
149 | return isAcknowledged;
150 | }
151 |
152 | public void setAcknowledged(Boolean acknowledged) {
153 | isAcknowledged = acknowledged;
154 | }
155 |
156 | public Boolean getSensitive() {
157 | if (isSensitive == null) {
158 | return false;
159 | }
160 | return isSensitive;
161 | }
162 |
163 | public void setSensitive(Boolean sensitive) {
164 | isSensitive = sensitive;
165 | }
166 |
167 | public Boolean getRetired() {
168 | if (isRetired == null) {
169 | return false;
170 | }
171 | return isRetired;
172 | }
173 |
174 | public void setRetired(Boolean retired) {
175 | isRetired = retired;
176 | }
177 |
178 | public Boolean getFabricated() {
179 | if (isFabricated == null) {
180 | return false;
181 | }
182 | return isFabricated;
183 | }
184 |
185 | public void setFabricated(Boolean fabricated) {
186 | isFabricated = fabricated;
187 | }
188 |
189 | public Boolean getSpamList() {
190 | if (isSpamList == null) {
191 | return false;
192 | }
193 | return isSpamList;
194 | }
195 |
196 | public void setSpamList(Boolean spamList) {
197 | isSpamList = spamList;
198 | }
199 |
200 | public boolean hasAdditionalFlags() {
201 | return ! getVerified() || getFabricated() || getSensitive() || getSpamList() || getRetired();
202 | }
203 |
204 | public String getLogoPath() {
205 | return logoPath;
206 | }
207 |
208 | public void setLogoPath(String logoPath) {
209 | this.logoPath = logoPath;
210 | }
211 |
212 | public Long getLastChecked() {
213 | return lastChecked;
214 | }
215 |
216 | public void setLastChecked(Long lastCheckDate) {
217 | this.lastChecked = lastCheckDate;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/db/entities/BreachedSite.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.db.entities;
2 |
3 | import androidx.room.ColumnInfo;
4 | import androidx.room.Entity;
5 | import androidx.room.Ignore;
6 | import androidx.room.PrimaryKey;
7 |
8 | @Entity(tableName = "breached_sites")
9 | public class BreachedSite {
10 |
11 | @PrimaryKey
12 | @ColumnInfo(name="_id")
13 | private Long id;
14 | @ColumnInfo(name = "name")
15 | private String name;
16 | @ColumnInfo(name = "title")
17 | private String title;
18 | @ColumnInfo(name = "domain")
19 | private String domain;
20 | @ColumnInfo(name = "breach_date")
21 | private Long breachDate;
22 | @ColumnInfo(name = "added_date")
23 | private Long addedDate;
24 | @ColumnInfo(name = "modified_date")
25 | private Long modifiedDate;
26 | @ColumnInfo(name = "pwn_count")
27 | private Long pwnCount;
28 | @ColumnInfo(name = "description")
29 | private String description;
30 | @ColumnInfo(name = "data_classes")
31 | private String dataClasses;
32 | @ColumnInfo(name = "is_verified")
33 | private Boolean isVerified;
34 | @ColumnInfo(name = "is_sensitive")
35 | private Boolean isSensitive;
36 | @ColumnInfo(name = "is_retired")
37 | private Boolean isRetired;
38 | @ColumnInfo(name = "is_fabricated")
39 | private Boolean isFabricated;
40 | @ColumnInfo(name = "is_spam_list")
41 | private Boolean isSpamList;
42 | @ColumnInfo(name = "logo_path")
43 | private String logoPath;
44 | @Ignore
45 | private Boolean detailsVisible = false;
46 |
47 | public Long getId() {
48 | return id;
49 | }
50 |
51 | public void setId(Long id) {
52 | this.id = id;
53 | }
54 |
55 | public String getName() {
56 | return name;
57 | }
58 |
59 | public void setName(String name) {
60 | this.name = name;
61 | }
62 |
63 | public String getTitle() {
64 | return title;
65 | }
66 |
67 | public void setTitle(String title) {
68 | this.title = title;
69 | }
70 |
71 | public String getDomain() {
72 | return domain;
73 | }
74 |
75 | public void setDomain(String domain) {
76 | this.domain = domain;
77 | }
78 |
79 | public Long getBreachDate() {
80 | return breachDate;
81 | }
82 |
83 | public void setBreachDate(Long breachDate) {
84 | this.breachDate = breachDate;
85 | }
86 |
87 | public Long getAddedDate() {
88 | return addedDate;
89 | }
90 |
91 | public void setAddedDate(Long addedDate) {
92 | this.addedDate = addedDate;
93 | }
94 |
95 | public Long getModifiedDate() {
96 | return modifiedDate;
97 | }
98 |
99 | public void setModifiedDate(Long modifiedDate) {
100 | this.modifiedDate = modifiedDate;
101 | }
102 |
103 | public Long getPwnCount() {
104 | return pwnCount;
105 | }
106 |
107 | public void setPwnCount(Long pwnCount) {
108 | this.pwnCount = pwnCount;
109 | }
110 |
111 | public String getDescription() {
112 | return description;
113 | }
114 |
115 | public void setDescription(String description) {
116 | this.description = description;
117 | }
118 |
119 | public String getDataClasses() {
120 | return dataClasses;
121 | }
122 |
123 | public void setDataClasses(String dataClasses) {
124 | this.dataClasses = dataClasses;
125 | }
126 |
127 | public Boolean getVerified() {
128 | if (isVerified == null) {
129 | return false;
130 | }
131 | return isVerified;
132 | }
133 |
134 | public void setVerified(Boolean verified) {
135 | isVerified = verified;
136 | }
137 |
138 | public Boolean getDetailsVisible() {
139 | return detailsVisible;
140 | }
141 |
142 | public void setDetailsVisible(Boolean detailsVisible) {
143 | this.detailsVisible = detailsVisible;
144 | }
145 |
146 | public Boolean getSensitive() {
147 | if (isSensitive == null) {
148 | return false;
149 | }
150 | return isSensitive;
151 | }
152 |
153 | public void setSensitive(Boolean sensitive) {
154 | isSensitive = sensitive;
155 | }
156 |
157 | public Boolean getRetired() {
158 | if (isRetired == null) {
159 | return false;
160 | }
161 | return isRetired;
162 | }
163 |
164 | public void setRetired(Boolean retired) {
165 | isRetired = retired;
166 | }
167 |
168 | public Boolean getFabricated() {
169 | if (isFabricated == null) {
170 | return false;
171 | }
172 | return isFabricated;
173 | }
174 |
175 | public void setFabricated(Boolean fabricated) {
176 | isFabricated = fabricated;
177 | }
178 |
179 | public Boolean getSpamList() {
180 | if (isSpamList == null) {
181 | return false;
182 | }
183 | return isSpamList;
184 | }
185 |
186 | public void setSpamList(Boolean spamList) {
187 | isSpamList = spamList;
188 | }
189 |
190 | public boolean hasAdditionalFlags() {
191 | return ! getVerified() || getFabricated() || getSensitive() || getSpamList() || getRetired();
192 | }
193 |
194 | public String getLogoPath() {
195 | return logoPath;
196 | }
197 |
198 | public void setLogoPath(String logoPath) {
199 | this.logoPath = logoPath;
200 | }
201 |
202 | }
203 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/services/FirebaseMessagagingService.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.services;
2 |
3 | import android.app.Notification;
4 | import android.app.PendingIntent;
5 | import android.content.Intent;
6 | import android.os.Build;
7 | import android.util.Log;
8 |
9 | import androidx.core.app.NotificationCompat;
10 | import androidx.work.Constraints;
11 | import androidx.work.Data;
12 | import androidx.work.ExistingWorkPolicy;
13 | import androidx.work.NetworkType;
14 | import androidx.work.OneTimeWorkRequest;
15 | import androidx.work.WorkManager;
16 |
17 | import com.google.firebase.messaging.FirebaseMessagingService;
18 | import com.google.firebase.messaging.RemoteMessage;
19 | import com.google.gson.Gson;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 | import java.util.Map;
24 |
25 | import li.doerf.hacked.activities.NavActivity;
26 | import li.doerf.hacked.remote.hibp.HIBPAccountResponseWorker;
27 | import li.doerf.hacked.utils.NotificationHelper;
28 | import li.doerf.hacked.utils.OreoNotificationHelper;
29 |
30 | public class FirebaseMessagagingService extends FirebaseMessagingService {
31 |
32 | private static final String TAG = "FirebaseMessagagingServ";
33 |
34 | @Override
35 | public void onMessageReceived(RemoteMessage remoteMessage) {
36 | Log.d(TAG, "received message: " + remoteMessage.getData().toString());
37 | if (remoteMessage.getData().containsKey("type")) {
38 | if (remoteMessage.getData().get("type").equals("hibp-response")) {
39 | Log.d(TAG, "processing hibp-response");
40 | processHibpResponse(remoteMessage.getData().get("account"), remoteMessage.getData().get("response"));
41 | }
42 | } else if (remoteMessage.getNotification() != null) {
43 | showNotification(remoteMessage.getNotification());
44 | }
45 | }
46 |
47 | private void processHibpResponse(String account, String responseStr) {
48 | List> response = new Gson().fromJson(responseStr, List.class);
49 | List breaches = new ArrayList<>();
50 | for(Map e : response) {
51 | breaches.add((String) e.get("Name"));
52 | }
53 |
54 | Data inputData = new Data.Builder()
55 | .putStringArray(HIBPAccountResponseWorker.KEY_BREACHES, breaches.toArray(new String[]{}))
56 | .putString(HIBPAccountResponseWorker.KEY_ACCOUNT, account)
57 | .build();
58 |
59 | Constraints constraints = new Constraints.Builder()
60 | .setRequiredNetworkType(NetworkType.CONNECTED)
61 | .build();
62 |
63 | OneTimeWorkRequest workerRequest = new OneTimeWorkRequest.Builder(HIBPAccountResponseWorker.class)
64 | .setInputData(inputData)
65 | .setConstraints(constraints)
66 | .build();
67 | WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork("hibp-response", ExistingWorkPolicy.APPEND, workerRequest);
68 | }
69 |
70 | private void showNotification(RemoteMessage.Notification remoteNotification) {
71 | if ( android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
72 | OreoNotificationHelper onh = new OreoNotificationHelper(getApplicationContext());
73 | onh.createGeneralNotificationChannel();
74 | }
75 |
76 | androidx.core.app.NotificationCompat.Builder mBuilder =
77 | new NotificationCompat.Builder(getApplicationContext(), OreoNotificationHelper.CHANNEL_ID)
78 | .setSmallIcon(android.R.drawable.sym_def_app_icon)
79 | .setContentTitle(remoteNotification.getTitle())
80 | .setContentText(remoteNotification.getBody())
81 | .setChannelId(OreoNotificationHelper.CHANNEL_ID_GENERAL)
82 | .setOnlyAlertOnce(true)
83 | .setAutoCancel(true);
84 |
85 | // TODO navigate directly to breaches
86 | Intent showBreachDetails = new Intent(getApplicationContext(), NavActivity.class);
87 | PendingIntent resultPendingIntent =
88 | PendingIntent.getActivity(
89 | getApplicationContext(),
90 | 0,
91 | showBreachDetails,
92 | PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
93 | );
94 | mBuilder.setContentIntent(resultPendingIntent);
95 |
96 | Notification notification = mBuilder.build();
97 | NotificationHelper.notify(getApplicationContext(), notification);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/ui/HibpInfo.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui;
2 |
3 | import android.content.Context;
4 | import android.text.Html;
5 | import android.text.method.LinkMovementMethod;
6 | import android.view.View;
7 | import android.view.animation.Animation;
8 | import android.view.animation.AnimationUtils;
9 | import android.widget.TextView;
10 |
11 | import androidx.recyclerview.widget.RecyclerView;
12 | import li.doerf.hacked.R;
13 |
14 | /**
15 | * Created by moo on 01/11/16.
16 | */
17 | public class HibpInfo {
18 |
19 | public static void prepare(final Context aContext, final TextView hibpInfo, RecyclerView aRecyclerView) {
20 | String text = aContext.getString(R.string.data_provided_by) + " Have i been pwned?";
21 | hibpInfo.setMovementMethod(LinkMovementMethod.getInstance());
22 | hibpInfo.setText(Html.fromHtml(text));
23 |
24 | if ( aRecyclerView != null ) {
25 | aRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
26 | @Override
27 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
28 | // show animation only once
29 | if (hibpInfo.getVisibility() == View.GONE) {
30 | return;
31 | }
32 |
33 | // animation running ... ignore
34 | if (hibpInfo.getAnimation() != null) {
35 | return;
36 | }
37 |
38 | // animate hipb info out of page
39 | Animation a = AnimationUtils.loadAnimation(aContext, R.anim.slide_out);
40 | a.setAnimationListener(new Animation.AnimationListener() {
41 | @Override
42 | public void onAnimationStart(Animation animation) {
43 |
44 | }
45 |
46 | @Override
47 | public void onAnimationEnd(Animation animation) {
48 | hibpInfo.setVisibility(View.GONE);
49 | }
50 |
51 | @Override
52 | public void onAnimationRepeat(Animation animation) {
53 |
54 | }
55 | });
56 | hibpInfo.startAnimation(a);
57 | }
58 | });
59 | }
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/ui/fragments/SettingsFragment.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.fragments;
2 |
3 |
4 | import android.content.SharedPreferences;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.preference.Preference;
8 | import android.preference.PreferenceFragment;
9 | import android.util.Log;
10 |
11 | import li.doerf.hacked.BuildConfig;
12 | import li.doerf.hacked.CustomEvent;
13 | import li.doerf.hacked.R;
14 | import li.doerf.hacked.util.Analytics;
15 | import li.doerf.hacked.util.FcmTokenManager;
16 | import li.doerf.hacked.utils.SynchronizationHelper;
17 |
18 |
19 | /**
20 | * Created by moo on 01/12/15.
21 | */
22 | public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
23 | private final String LOGTAG = getClass().getSimpleName();
24 |
25 | @Override
26 | public void onCreate(Bundle savedInstanceState) {
27 | super.onCreate(savedInstanceState);
28 |
29 | // Load the preferences from an XML resource
30 | addPreferencesFromResource(R.xml.preferences);
31 |
32 | Preference versionPreference = findPreference("version");
33 | versionPreference.setSummary(String.format("%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
34 |
35 | Preference devicePreference = findPreference("device");
36 | devicePreference.setSummary(String.format("%s %s / API %s", Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT));
37 |
38 | Preference cleanTokenPreference = findPreference("clean_token");
39 | cleanTokenPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
40 | @Override
41 | public boolean onPreferenceClick(Preference preference) {
42 | FcmTokenManager.Companion.cleanToken(getContext());
43 | return true;
44 | }
45 | });
46 | }
47 |
48 | @Override
49 | public void onResume() {
50 | super.onResume();
51 | getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
52 | Analytics.Companion.trackView("Fragment~Settings");
53 | }
54 |
55 | @Override
56 | public void onPause() {
57 | getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
58 | super.onPause();
59 | }
60 |
61 | @Override
62 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
63 | Log.d(LOGTAG, "preference changed: " + key);
64 | if ( getString(R.string.pref_key_sync_enable).equals( key) ||
65 | getString( R.string.pref_key_sync_interval).equals( key) ||
66 | getString(R.string.pref_key_sync_via_cellular).equals(key)
67 | ) {
68 | boolean enabled = SynchronizationHelper.scheduleSync(getActivity().getApplicationContext());
69 |
70 | if ( enabled) {
71 | Analytics.Companion.trackCustomEvent(CustomEvent.BACKGROUND_SYNC_ENABLED);
72 | } else {
73 | Analytics.Companion.trackCustomEvent(CustomEvent.BACKGROUND_SYNC_DISABLED);
74 | }
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/ui/viewmodels/AccountViewModel.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.viewmodels;
2 |
3 | import android.app.Application;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.lifecycle.AndroidViewModel;
7 | import androidx.lifecycle.LiveData;
8 |
9 | import java.util.List;
10 |
11 | import li.doerf.hacked.db.AppDatabase;
12 | import li.doerf.hacked.db.entities.Account;
13 |
14 | public class AccountViewModel extends AndroidViewModel {
15 |
16 | private final LiveData> accountList;
17 | private final LiveData lastChecked;
18 |
19 | public AccountViewModel(@NonNull Application application) {
20 | super(application);
21 | accountList = AppDatabase.get(application).getAccountDao().getAllLD();
22 | lastChecked = AppDatabase.get(application).getAccountDao().getLastChecked();
23 | }
24 |
25 | public LiveData> getAccountList() {
26 | return accountList;
27 | }
28 |
29 | public LiveData getLastChecked() {
30 | return lastChecked;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/ui/viewmodels/BreachViewModel.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.viewmodels;
2 |
3 | import android.app.Application;
4 | import android.util.LongSparseArray;
5 |
6 | import java.util.List;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.lifecycle.AndroidViewModel;
10 | import androidx.lifecycle.LiveData;
11 | import li.doerf.hacked.db.AppDatabase;
12 | import li.doerf.hacked.db.daos.BreachDao;
13 | import li.doerf.hacked.db.entities.Breach;
14 |
15 | public class BreachViewModel extends AndroidViewModel {
16 |
17 | private final BreachDao myBreachDao;
18 | private final LongSparseArray>> breachListMap = new LongSparseArray<>();
19 |
20 | public BreachViewModel(@NonNull Application application) {
21 | super(application);
22 | myBreachDao = AppDatabase.get(getApplication()).getBreachDao();
23 | }
24 |
25 | public LiveData> getBreachList(Long accountId) {
26 | if (breachListMap.indexOfKey(accountId) < 0) {
27 | breachListMap.put(accountId, myBreachDao.findByAccountLD(accountId));
28 | }
29 |
30 | return breachListMap.get(accountId);
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/utils/AccountHelper.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.utils;
2 |
3 | import android.content.Context;
4 |
5 | import java.util.ArrayList;
6 | import java.util.Collection;
7 | import java.util.List;
8 |
9 | import li.doerf.hacked.db.AppDatabase;
10 | import li.doerf.hacked.db.daos.BreachDao;
11 | import li.doerf.hacked.db.entities.Account;
12 | import li.doerf.hacked.db.entities.Breach;
13 |
14 | public class AccountHelper {
15 |
16 | private final BreachDao myBreachDao;
17 |
18 | public AccountHelper(Context context) {
19 | myBreachDao = AppDatabase.get(context).getBreachDao();
20 | }
21 |
22 | public void updateBreachCounts(Account account) {
23 | List breaches = myBreachDao.findByAccount(account.getId());
24 | account.setNumBreaches(breaches.size());
25 | account.setNumAcknowledgedBreaches(getAcknowledged(breaches).size());
26 | }
27 |
28 | private Collection getAcknowledged(List breaches) {
29 | List result = new ArrayList<>();
30 | for(Breach b : breaches) {
31 | if(b.getAcknowledged()) {
32 | result.add(b);
33 | }
34 | }
35 | return result;
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/utils/NotificationHelper.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.utils;
2 |
3 | import android.app.Notification;
4 | import android.content.Context;
5 | import android.util.Log;
6 |
7 | import java.util.concurrent.atomic.AtomicInteger;
8 |
9 | import androidx.core.app.NotificationManagerCompat;
10 |
11 | /**
12 | * Created by moo on 04/12/15.
13 | */
14 | public class NotificationHelper {
15 | private final static String LOGTAG = "NotificationHelper";
16 | private final static AtomicInteger notifyId = new AtomicInteger();
17 |
18 | private static int getNotificationId() {
19 | return notifyId.incrementAndGet();
20 | }
21 |
22 | public static void notify(Context aContext, Notification aNotification) {
23 | // Sets an ID for the notification
24 | int notificationId = NotificationHelper.getNotificationId();
25 | notify(aContext, aNotification, notificationId);
26 | }
27 |
28 | private static void notify(Context aContext, Notification aNotification, int aNotificationId) {
29 | // Gets an instance of the NotificationManager service
30 | // NotificationManager notificationManager =
31 | // (NotificationManager) aContext.getSystemService(Context.NOTIFICATION_SERVICE);
32 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(aContext);
33 | // Builds the notification and issues it.
34 | notificationManager.notify(aNotificationId, aNotification);
35 | Log.d(LOGTAG, "notification build and issued: " + aNotificationId);
36 | }
37 |
38 | public static void cancelAll(Context aContext) {
39 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(aContext);
40 | notificationManager.cancelAll();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/utils/OreoNotificationHelper.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.utils;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.content.Context;
7 | import android.graphics.Color;
8 | import android.os.Build;
9 |
10 | import li.doerf.hacked.R;
11 |
12 | /**
13 | * Created by moo on 08.10.17.
14 | */
15 | @TargetApi(Build.VERSION_CODES.O)
16 | public class OreoNotificationHelper {
17 |
18 | private final Context myContext;
19 | public static final String CHANNEL_ID = "my_channel_01";
20 | public static final String CHANNEL_ID_GENERAL = "my_channel_02";
21 |
22 | public OreoNotificationHelper(Context aContext) {
23 | myContext = aContext;
24 | }
25 |
26 | public void createNotificationChannel() {
27 | NotificationManager mNotificationManager =
28 | (NotificationManager) myContext.getSystemService(Context.NOTIFICATION_SERVICE);
29 | // The id of the channel.
30 | // The user-visible name of the channel.
31 | CharSequence name = myContext.getString(R.string.channel_name);
32 | // The user-visible description of the channel.
33 | int importance = NotificationManager.IMPORTANCE_DEFAULT;
34 | NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
35 | // Configure the notification channel.
36 | // mChannel.setDescription(description);
37 | mChannel.enableLights(true);
38 | // Sets the notification light color for notifications posted to this
39 | // channel, if the device supports this feature.
40 | mChannel.setLightColor(Color.RED);
41 | if (mNotificationManager == null ) { return; }
42 | mNotificationManager.createNotificationChannel(mChannel);
43 | }
44 |
45 | public void createGeneralNotificationChannel() {
46 | NotificationManager mNotificationManager =
47 | (NotificationManager) myContext.getSystemService(Context.NOTIFICATION_SERVICE);
48 | // The id of the channel.
49 | // The user-visible name of the channel.
50 | CharSequence name = myContext.getString(R.string.channel_name_general);
51 | // The user-visible description of the channel.
52 | int importance = NotificationManager.IMPORTANCE_DEFAULT;
53 | NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_GENERAL, name, importance);
54 | // Configure the notification channel.
55 | // mChannel.setDescription(description);
56 | mChannel.enableLights(true);
57 | // Sets the notification light color for notifications posted to this
58 | // channel, if the device supports this feature.
59 | mChannel.setLightColor(Color.RED);
60 | if (mNotificationManager == null ) { return; }
61 | mNotificationManager.createNotificationChannel(mChannel);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/utils/StringHelper.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.utils;
2 |
3 | import java.text.NumberFormat;
4 | import java.util.Arrays;
5 | import java.util.Collection;
6 | import java.util.Iterator;
7 | import java.util.Locale;
8 |
9 | /**
10 | * Created by moo on 23.02.18.
11 | */
12 |
13 | public class StringHelper {
14 |
15 | public static String addDigitSeperator(String aNumberString) {
16 | return NumberFormat.getNumberInstance(Locale.US).format(Long.parseLong(aNumberString));
17 | }
18 |
19 | public static String join(String[] strings, String joinStr) {
20 | return join(Arrays.asList(strings), joinStr);
21 | }
22 |
23 | private static String join(Collection strings, String joinStr) {
24 | if (strings.size() == 0) {
25 | return "";
26 | }
27 | StringBuilder b = new StringBuilder();
28 | Iterator iter = strings.iterator();
29 | b.append(iter.next());
30 | while(iter.hasNext()) {
31 | b.append(joinStr);
32 | b.append(iter.next());
33 | }
34 | return b.toString();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/li/doerf/hacked/utils/SynchronizationHelper.java:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.utils;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.preference.PreferenceManager;
6 | import android.util.Log;
7 |
8 | import androidx.work.Constraints;
9 | import androidx.work.ExistingPeriodicWorkPolicy;
10 | import androidx.work.NetworkType;
11 | import androidx.work.PeriodicWorkRequest;
12 | import androidx.work.WorkInfo;
13 | import androidx.work.WorkManager;
14 |
15 | import java.util.List;
16 | import java.util.concurrent.ExecutionException;
17 | import java.util.concurrent.TimeUnit;
18 |
19 | import li.doerf.hacked.R;
20 | import li.doerf.hacked.remote.hibp.HIBPAccountCheckerWorker;
21 |
22 | /**
23 | * Created by moo on 08/09/16.
24 | */
25 | public class SynchronizationHelper {
26 | private static final String LOGTAG = "SynchronizationHelper";
27 | private static final String JOB_TAG = "hacked-background-check-job";
28 |
29 | public static void setupInitialSync(Context aContext) {
30 | boolean isScheduled = false;
31 | try {
32 | List workinfos = WorkManager.getInstance(aContext).getWorkInfosByTag(JOB_TAG).get();
33 | for (WorkInfo wi : workinfos) {
34 | WorkInfo.State state = wi.getState();
35 | if( state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED) {
36 | Log.d(LOGTAG, "found scheduled work");
37 | isScheduled = true;
38 | }
39 | }
40 | } catch (ExecutionException e) {
41 | Log.e(LOGTAG, "caught ExecutionException while checking status", e);
42 | } catch (InterruptedException e) {
43 | Log.e(LOGTAG, "caught InterruptedException while checking status", e);
44 | }
45 | if (!isScheduled) {
46 | Log.i(LOGTAG, "no background work scheduled - need to schedule new background worker");
47 | scheduleSync(aContext);
48 | }
49 | }
50 |
51 |
52 | public static boolean scheduleSync(Context aContext) {
53 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(aContext);
54 | disableSync(aContext);
55 | boolean enabled = false;
56 |
57 | if (settings.getBoolean(aContext.getString(R.string.pref_key_sync_enable), true)) {
58 | enableSync(aContext);
59 | enabled = true;
60 | }
61 |
62 | return enabled;
63 | }
64 |
65 | private static void enableSync(Context aContext) {
66 | Log.d(LOGTAG, "scheduling synchronization");
67 |
68 | int currentIntervalHours = getCurrentIntervalHours(aContext);
69 |
70 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(aContext);
71 | boolean runCheckOnCellular = settings.getBoolean(aContext.getString(R.string.pref_key_sync_via_cellular), false);
72 | NetworkType networkType = runCheckOnCellular ? NetworkType.METERED : NetworkType.UNMETERED;
73 |
74 | Constraints constraints = new Constraints.Builder()
75 | .setRequiredNetworkType(networkType)
76 | .build();
77 |
78 | PeriodicWorkRequest.Builder checkWorker =
79 | new PeriodicWorkRequest.Builder(HIBPAccountCheckerWorker.class, currentIntervalHours,
80 | TimeUnit.HOURS)
81 | .addTag(JOB_TAG)
82 | .setConstraints(constraints);
83 |
84 | PeriodicWorkRequest photoCheckWork = checkWorker.build();
85 | WorkManager.getInstance(aContext).enqueueUniquePeriodicWork(JOB_TAG, ExistingPeriodicWorkPolicy.KEEP, photoCheckWork);
86 |
87 | Log.i(LOGTAG, "scheduled job");
88 | }
89 |
90 | private static void disableSync(Context aContext) {
91 | Log.d(LOGTAG, "unscheduling synchronization");
92 | WorkManager.getInstance(aContext).cancelAllWorkByTag(JOB_TAG);
93 | Log.i(LOGTAG, "unscheduled sync job");
94 | }
95 |
96 | /**
97 | * Get the current check interval in hours
98 | * @param aContext the context
99 | * @return the current check interval in hours
100 | */
101 | private static int getCurrentIntervalHours(Context aContext) {
102 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(aContext);
103 | String intervalString = settings.getString(aContext.getString(R.string.pref_key_sync_interval), "everyday");
104 |
105 | switch ( intervalString) {
106 | case "everyday":
107 | return 24;
108 |
109 | case "everytwodays":
110 | return 24 * 2;
111 |
112 | case "everythreedays":
113 | return 24 * 3;
114 |
115 | case "everyweek":
116 | return 24 * 7;
117 |
118 | default:
119 | return 24;
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/CustomEvent.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked
2 |
3 | enum class CustomEvent {
4 | FIRST_ACCOUNT_ADDED,
5 | ACCOUNT_ADDED,
6 | ACCOUNT_DELETED,
7 | CHECK_FOR_BREACHES,
8 | RELOAD_BREACHED_SITES,
9 | BREACH_ACKNOWLEDGED,
10 | RATE_NOW,
11 | RATE_LATER,
12 | RATE_NEVER,
13 | CHECK_PASSWORD_PWNED,
14 | BACKGROUND_SYNC_ENABLED,
15 | BACKGROUND_SYNC_DISABLED,
16 | DISMISS_INITIAL_HELP,
17 | PASSWORD_PWNED_EXCEPTION,
18 | PASSWORD_NOT_PWNED,
19 | PASSWORD_PWNED,
20 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/activities/NavActivity.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.ActivityNotFoundException
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.view.Menu
10 | import android.view.MenuItem
11 | import android.widget.Toast
12 | import android.widget.Toast.makeText
13 | import androidx.appcompat.app.AppCompatActivity
14 | import androidx.appcompat.widget.Toolbar
15 | import androidx.navigation.NavController
16 | import androidx.navigation.Navigation
17 | import androidx.navigation.ui.AppBarConfiguration
18 | import androidx.navigation.ui.NavigationUI
19 | import com.google.firebase.crashlytics.FirebaseCrashlytics
20 | import io.reactivex.processors.PublishProcessor
21 | import li.doerf.hacked.HackedApplication
22 | import li.doerf.hacked.R
23 | import li.doerf.hacked.ui.fragments.AccountsFragmentDirections
24 | import li.doerf.hacked.ui.fragments.FirstUseFragmentDirections
25 | import li.doerf.hacked.ui.fragments.OverviewFragmentDirections
26 | import li.doerf.hacked.util.NavEvent
27 |
28 |
29 | class NavActivity : AppCompatActivity() {
30 |
31 | lateinit var navEvents: PublishProcessor
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | navEvents = (applicationContext as HackedApplication).navEvents
36 | setContentView(R.layout.activity_nav)
37 | val toolbar = findViewById(R.id.toolbar)
38 | setSupportActionBar(toolbar)
39 | toolbar.title = getString(R.string.app_name)
40 | title = getString(R.string.app_name)
41 | val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
42 | NavigationUI.setupActionBarWithNavController(this, navController)
43 |
44 | setupNavigation(navController)
45 | }
46 |
47 | @SuppressLint("CheckResult")
48 | private fun setupNavigation(navController: NavController) {
49 | navEvents.subscribe {
50 | when (it.destination) {
51 | NavEvent.Destination.OVERVIEW -> if(navController.currentDestination?.id == R.id.firstUseFragment) {
52 | navController.navigate(FirstUseFragmentDirections.actionFirstUseFragmentToOverviewFragment())
53 | }
54 | NavEvent.Destination.FIRST_USE -> if(navController.currentDestination?.id == R.id.overviewFragment) {
55 | navController.navigate(OverviewFragmentDirections.actionOverviewFragmentToFirstUseFragment())
56 | }
57 | NavEvent.Destination.ACCOUNTS_DETAILS -> when(navController.currentDestination?.id) {
58 | R.id.overviewFragment -> {
59 | val action = OverviewFragmentDirections.actionOverviewFragmentToAccountDetailsFragment()
60 | action.accountId = it.id!!
61 | navController.navigate(action)
62 | }
63 | R.id.accountsListFullFragment -> {
64 | navController.navigate(AccountsFragmentDirections.actionAccountsListFullFragmentToAccountDetailsFragment(it.id!!))
65 | }
66 | }
67 | NavEvent.Destination.ACCOUNTS_LIST -> if(navController.currentDestination?.id == R.id.overviewFragment) {
68 | navController.navigate(OverviewFragmentDirections.actionOverviewFragmentToAccountsListFullFragment())
69 | }
70 | NavEvent.Destination.ALL_BREACHES -> if(navController.currentDestination?.id == R.id.overviewFragment) {
71 | val action = OverviewFragmentDirections.actionOverviewFragmentToAllBreachesFragment()
72 | if (it.id != null) {
73 | action.breachedSiteId = it.id
74 | }
75 | navController.navigate(action)
76 | }
77 | NavEvent.Destination.PWNED_PASSWORDS -> if(navController.currentDestination?.id == R.id.overviewFragment) {
78 | navController.navigate(OverviewFragmentDirections.actionOverviewFragmentToPwnedPasswordFragment(it.string!!))
79 | }
80 | }
81 | }
82 | }
83 |
84 | override fun onSupportNavigateUp(): Boolean {
85 | val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
86 | val appBarConfiguration = AppBarConfiguration(navController.graph)
87 | return NavigationUI.navigateUp(navController, appBarConfiguration)
88 | }
89 |
90 | override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present.
91 | menuInflater.inflate(R.menu.menu_nav, menu)
92 | return true
93 | }
94 |
95 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
96 | return when(item.itemId) {
97 | R.id.action_privacypolicy -> {
98 | try {
99 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://doerfli.github.io/hacked/privacy"))
100 | startActivity(browserIntent)
101 | } catch (e: ActivityNotFoundException) {
102 | Log.e(TAG, "caught ActivityNotFoundException", e)
103 | FirebaseCrashlytics.getInstance().recordException(e)
104 | makeText(applicationContext, getString(R.string.unable_to_start_browser, "https://doerfli.github.io/hacked/privacy"), Toast.LENGTH_LONG).show()
105 | }
106 | true
107 | }
108 | R.id.action_visit_hibp -> {
109 | try {
110 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://haveibeenpwned.com"))
111 | startActivity(browserIntent)
112 | } catch (e: ActivityNotFoundException) {
113 | Log.e(TAG, "caught ActivityNotFoundException", e)
114 | FirebaseCrashlytics.getInstance().recordException(e)
115 | makeText(applicationContext, getString(R.string.unable_to_start_browser, "https://haveibeenpwned.com"), Toast.LENGTH_LONG).show()
116 | }
117 | true
118 | }
119 | R.id.action_settings -> {
120 | val settingsIntent = Intent(this, SettingsActivity::class.java)
121 | startActivity(settingsIntent)
122 | true
123 | }
124 | else -> {
125 | super.onOptionsItemSelected(item)
126 | }
127 | }
128 | }
129 |
130 | companion object {
131 | private val TAG = "NavActivity"
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/initializer/BackgroundSyncInitializer.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.initializer
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.startup.Initializer
6 | import androidx.work.WorkManagerInitializer
7 | import li.doerf.hacked.utils.SynchronizationHelper
8 |
9 | class BackgroundSyncInitializer : Initializer {
10 |
11 | override fun create(context: Context): String {
12 | SynchronizationHelper.setupInitialSync(context)
13 | Log.i(TAG, "initialized")
14 | return "sync initialized"
15 | }
16 |
17 | override fun dependencies(): List>> {
18 | return listOf(WorkManagerInitializer::class.java)
19 | }
20 |
21 | companion object {
22 | private const val TAG = "BackgroundSyncInitializ"
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/initializer/FirebaseAnalyticsInitializer.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.initializer
2 |
3 | import android.content.Context
4 | import android.provider.Settings
5 | import android.util.Log
6 | import androidx.startup.Initializer
7 | import li.doerf.hacked.util.Analytics
8 |
9 | class FirebaseAnalyticsInitializer : Initializer {
10 |
11 | override fun create(context: Context): String {
12 | Analytics.initialize(context, Settings.System.getString(context.contentResolver, "firebase.test.lab"))
13 | Log.i(TAG, "initialized")
14 | return "firebaseanalyticsinitialized"
15 | }
16 |
17 | override fun dependencies(): MutableList>> {
18 | return mutableListOf()
19 | }
20 |
21 | companion object {
22 | private const val TAG = "FirebaseAnalyticsInitia"
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/remote/hibp/BreachedAccount.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.remote.hibp
2 |
3 | /**
4 | * Created by moo on 05/09/16.
5 | */
6 | data class BreachedAccount(
7 | var name: String? = null,
8 | var title: String? = null,
9 | var domain: String? = null,
10 | var breachDate: String? = null,
11 | var addedDate: String? = null,
12 | var modifiedDate: String? = null,
13 | var pwnCount: Long? = null,
14 | var description: String? = null,
15 | var dataClasses: Array? = null,
16 | var isVerified: Boolean? = null,
17 | var isSensitive: Boolean? = null,
18 | var isRetired: Boolean? = null,
19 | var isFabricated: Boolean? = null,
20 | var IsSpamList: Boolean? = null,
21 | var LogoPath: String? = null
22 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/remote/hibp/WorkFailedException.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.remote.hibp
2 |
3 | class WorkFailedException(val retry : Boolean = false) : Exception()
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/remote/pwnedpasswords/PwnedPassword.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.remote.pwnedpasswords
2 |
3 | import android.content.Intent
4 | import android.util.Log
5 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
6 | import com.github.kittinunf.fuel.core.FuelError
7 | import com.github.kittinunf.fuel.core.isSuccessful
8 | import com.github.kittinunf.fuel.coroutines.awaitStringResponseResult
9 | import com.github.kittinunf.fuel.httpGet
10 | import com.google.firebase.crashlytics.FirebaseCrashlytics
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Job
13 | import kotlinx.coroutines.launch
14 | import li.doerf.hacked.util.logException
15 | import org.apache.commons.codec.binary.Hex
16 | import org.apache.commons.codec.digest.DigestUtils
17 | import java.util.*
18 |
19 | class PwnedPassword(private val broadcastManager: LocalBroadcastManager) {
20 |
21 | companion object {
22 | const val TAG = "PwnedPassword"
23 | const val BROADCAST_ACTION_PASSWORD_PWNED = "li.doerf.hacked.BROADCAST_ACTION_PASSWORD_PWNED"
24 | const val EXTRA_PASSWORD_PWNED = "ExtraPwned"
25 | const val EXTRA_PASSWORD_PWNED_Count = "ExtraPwnedNum"
26 | const val EXTRA_EXCEPTION = "ExtraException"
27 | const val URL = "https://api.pwnedpasswords.com/range"
28 | }
29 |
30 | fun check(password: String) {
31 | CoroutineScope(Job()).launch {
32 | try {
33 | checkPassword(password)
34 | } catch (e: FuelError) {
35 | FirebaseCrashlytics.getInstance().recordException(e)
36 | logException(TAG, Log.ERROR, e, "caught FuelError during pwned password check")
37 | notifyException()
38 | }
39 | }
40 | }
41 |
42 | private suspend fun checkPassword(password: String) {
43 | val pwdHash = String(Hex.encodeHex(DigestUtils.sha1(password))).toUpperCase(Locale.getDefault())
44 | val pwdHashHead = pwdHash.substring(0, 5)
45 |
46 | Log.d(TAG, "checking password: ")
47 | val (_, res, result) = "$URL/$pwdHashHead".httpGet().awaitStringResponseResult()
48 | Log.d(TAG, "status: ${res.statusCode}")
49 | if (!res.isSuccessful) {
50 | Log.w(TAG, result.component2())
51 | Log.w(TAG, res.toString())
52 | notifyException()
53 | return
54 | }
55 |
56 | val pwnedCount = processResult(result.get(), pwdHashHead, pwdHash)
57 |
58 | if (pwnedCount > -1) {
59 | notifyPwned(pwnedCount)
60 | } else {
61 | notifyNotPwned()
62 | }
63 | }
64 |
65 | private fun processResult(result: String, pwdHashHead: String, pwdHash: String): Int {
66 | var pwnedCount = -1
67 | result.split("\r\n").forEach { line ->
68 | if (!line.contains(":")) {
69 | return@forEach
70 | }
71 | val (e, numPwns, _) = line.split(":")
72 | val hash = "$pwdHashHead$e"
73 | Log.d(TAG, "$hash $numPwns")
74 | if (pwdHash != hash) {
75 | return@forEach
76 | }
77 | if (numPwns.contains(',')) {
78 | numPwns.replace(",", "")
79 | }
80 | pwnedCount = Integer.parseInt(numPwns)
81 | }
82 | return pwnedCount
83 | }
84 |
85 | private fun notifyPwned(pwnedCount: Int) {
86 | val localIntent = Intent(BROADCAST_ACTION_PASSWORD_PWNED)
87 | localIntent.putExtra(EXTRA_PASSWORD_PWNED, true)
88 | localIntent.putExtra(EXTRA_PASSWORD_PWNED_Count, pwnedCount)
89 | notify(localIntent)
90 | }
91 |
92 | private fun notifyNotPwned() {
93 | val localIntent = Intent(BROADCAST_ACTION_PASSWORD_PWNED)
94 | localIntent.putExtra(EXTRA_PASSWORD_PWNED, false)
95 | notify(localIntent)
96 | }
97 |
98 | private fun notifyException() {
99 | val localIntent = Intent(BROADCAST_ACTION_PASSWORD_PWNED)
100 | localIntent.putExtra(EXTRA_EXCEPTION, true)
101 | notify(localIntent)
102 | }
103 |
104 | private fun notify(intent: Intent) {
105 | broadcastManager.sendBroadcast(intent)
106 | Log.d(TAG, "broadcast finish sent")
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/services/AccountService.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.services
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.util.Log
6 | import android.widget.Toast
7 | import androidx.work.*
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.runBlocking
11 | import li.doerf.hacked.CustomEvent
12 | import li.doerf.hacked.R
13 | import li.doerf.hacked.db.AppDatabase
14 | import li.doerf.hacked.db.daos.AccountDao
15 | import li.doerf.hacked.db.entities.Account
16 | import li.doerf.hacked.remote.hibp.HIBPAccountCheckerWorker
17 | import li.doerf.hacked.ui.fragments.AccountsFragment
18 | import li.doerf.hacked.util.Analytics
19 | import li.doerf.hacked.util.createCoroutingExceptionHandler
20 |
21 | class AccountService(private val application: Application) {
22 |
23 | private var context: Context = application.applicationContext
24 |
25 | fun addAccount(aName: String) {
26 | if ( aName.trim { it <= ' ' } == "") {
27 | Toast.makeText(context, context.getString(R.string.toast_enter_valid_name), Toast.LENGTH_LONG).show()
28 | Log.w(AccountsFragment.LOGTAG, "account name not valid")
29 | return
30 | }
31 | val name = aName.trim { it <= ' ' }
32 |
33 | runBlocking(context = Dispatchers.IO) {
34 | launch(createCoroutingExceptionHandler(AccountsFragment.LOGTAG)) {
35 | addNewAccount(name)
36 | }
37 | }
38 | }
39 |
40 | private fun addNewAccount(name: String) {
41 | val accountDao = AppDatabase.get(context).accountDao
42 | val count = accountDao.countByName(name)
43 | if (count > 0) {
44 | return
45 | }
46 | insertAccount(accountDao, createNewAccount(name))
47 | }
48 |
49 | private fun createNewAccount(name: String): Account {
50 | val account = Account()
51 | account.name = name
52 | account.numBreaches = 0
53 | account.numAcknowledgedBreaches = 0
54 | return account
55 | }
56 |
57 | private fun insertAccount(accountDao: AccountDao, account: Account) {
58 | val ids = accountDao.insert(account)
59 | Analytics.trackCustomEvent(CustomEvent.ACCOUNT_ADDED)
60 | checkNewAccount(ids)
61 | }
62 |
63 | private fun checkNewAccount(ids: MutableList) {
64 | val inputData = Data.Builder()
65 | .putLong(HIBPAccountCheckerWorker.KEY_ID, ids[0])
66 | .build()
67 | val constraints = Constraints.Builder()
68 | .setRequiredNetworkType(NetworkType.UNMETERED)
69 | .build()
70 | val checker = OneTimeWorkRequest.Builder(HIBPAccountCheckerWorker::class.java)
71 | .setInputData(inputData)
72 | .setConstraints(constraints)
73 | .build()
74 | WorkManager.getInstance(context).enqueue(checker)
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/RateUsDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui
2 |
3 | import android.app.Dialog
4 | import android.content.DialogInterface
5 | import android.os.Bundle
6 | import android.util.Log
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.fragment.app.DialogFragment
9 | import androidx.preference.PreferenceManager
10 | import li.doerf.hacked.CustomEvent
11 | import li.doerf.hacked.R
12 | import li.doerf.hacked.util.Analytics
13 | import li.doerf.hacked.util.AppReview
14 | import li.doerf.hacked.util.RatingHelper
15 |
16 | class RateUsDialogFragment() : DialogFragment() {
17 | private val LOGTAG = javaClass.simpleName
18 | private lateinit var appReview: AppReview
19 |
20 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
21 | return AlertDialog.Builder(requireContext())
22 | .setTitle(getString(R.string.rating_dialog_title))
23 | .setMessage(getString(R.string.rating_dialog_message))
24 | .setPositiveButton(getString(R.string.rating_dialog_positive)) { _: DialogInterface?, _: Int -> handleClickPositive() }
25 | .setNeutralButton(getString(R.string.rating_dialog_neutral)) { _: DialogInterface?, _: Int -> handleClickNeutral() }
26 | .setNegativeButton(getString(R.string.rating_dialog_negative)) { _: DialogInterface?, _: Int -> handleClickNegative() }.create()
27 | }
28 |
29 | fun setAppReview(appRevie: AppReview) {
30 | this.appReview = appRevie
31 | }
32 |
33 | private fun handleClickPositive() {
34 | if (this::appReview.isInitialized) {
35 | appReview.showReview()
36 | } else {
37 | handleClickNeutral();
38 | }
39 | }
40 |
41 | private fun handleClickNeutral() {
42 | val settings = PreferenceManager.getDefaultSharedPreferences(requireContext())
43 | val editor = settings.edit()
44 | editor.putInt(RatingHelper.PREF_KEY_RATING_COUNTER, 0)
45 | editor.apply()
46 | Log.i(LOGTAG, "setting: reset rating counter")
47 | Analytics.trackCustomEvent(CustomEvent.RATE_LATER)
48 | }
49 |
50 | private fun handleClickNegative() {
51 | val settings = PreferenceManager.getDefaultSharedPreferences(requireContext())
52 | val editor = settings.edit()
53 | editor.putBoolean(RatingHelper.PREF_KEY_RATING_NEVER, true)
54 | editor.apply()
55 | Log.i(LOGTAG, "setting: never rate")
56 | Analytics.trackCustomEvent(CustomEvent.RATE_NEVER)
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/adapters/AccountsAdapter.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.adapters
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.TextView
9 | import androidx.recyclerview.widget.RecyclerView
10 | import io.reactivex.processors.PublishProcessor
11 | import li.doerf.hacked.HackedApplication
12 | import li.doerf.hacked.R
13 | import li.doerf.hacked.db.entities.Account
14 | import li.doerf.hacked.util.NavEvent
15 | import li.doerf.hacked.utils.NotificationHelper
16 | import java.util.*
17 |
18 |
19 | class AccountsAdapter(private val context: Context, private var accountList: List) : RecyclerView.Adapter() {
20 |
21 | private lateinit var navEvents: PublishProcessor
22 |
23 | override fun onViewAttachedToWindow(holder: RecyclerViewHolder) {
24 | super.onViewAttachedToWindow(holder)
25 | navEvents = (context as HackedApplication).navEvents
26 | }
27 |
28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
29 | val itemLayout = LayoutInflater.from(parent.context)
30 | .inflate(R.layout.card_account, parent, false)
31 | return RecyclerViewHolder(itemLayout)
32 | }
33 |
34 | override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
35 | val view = holder.view
36 | val account = accountList[position]
37 | bindView(view, account)
38 | }
39 |
40 | private fun bindView(view: View, account: Account) {
41 | val statusIndicator = view.findViewById(R.id.status_indicator)
42 | setStatusIndicatorColor(account, statusIndicator)
43 |
44 | val nameView = view.findViewById(R.id.name)
45 | nameView.text = account.name
46 |
47 | val breachCount = view.findViewById(R.id.breach_count)
48 | val breachCounter = account.numBreaches
49 | Log.d(LOGTAG, "breachCounter: ${breachCounter}")
50 |
51 | if (breachCounter > 0) {
52 | breachCount.text = String.format(Locale.getDefault(), "%d", breachCounter)
53 | breachCount.visibility = View.VISIBLE
54 | } else {
55 | breachCount.visibility = View.GONE
56 | }
57 |
58 | view.setOnClickListener {
59 | NotificationHelper.cancelAll(context)
60 | navEvents.onNext(NavEvent(NavEvent.Destination.ACCOUNTS_DETAILS, account.id, null))
61 | }
62 | }
63 |
64 | private fun setStatusIndicatorColor(account: Account, statusIndicator: View) {
65 | if (account.hacked) {
66 | statusIndicator.setBackgroundColor(context.resources.getColor(R.color.account_status_breached))
67 | } else if (!account.hacked && account.lastChecked == null) {
68 | statusIndicator.setBackgroundColor(context.resources.getColor(R.color.account_status_unknown))
69 | } else {
70 | if (account.numBreaches == 0) {
71 | statusIndicator.setBackgroundColor(context.resources.getColor(R.color.account_status_ok))
72 | } else {
73 | statusIndicator.setBackgroundColor(context.resources.getColor(R.color.account_status_only_acknowledged))
74 | }
75 | }
76 | }
77 |
78 | override fun getItemCount(): Int {
79 | return accountList.size
80 | }
81 |
82 | fun addItems(accountList: List) {
83 | this.accountList = accountList
84 | notifyDataSetChanged()
85 | }
86 |
87 | companion object {
88 | private val LOGTAG = this::class.java.name
89 | }
90 |
91 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/adapters/BreachedSitesAdapter.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.adapters
2 |
3 | import android.content.Context
4 | import android.text.Html
5 | import android.util.Log
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.ImageView
10 | import android.widget.RelativeLayout
11 | import android.widget.TextView
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.squareup.picasso.Picasso
14 | import io.reactivex.processors.PublishProcessor
15 | import li.doerf.hacked.HackedApplication
16 | import li.doerf.hacked.R
17 | import li.doerf.hacked.db.entities.BreachedSite
18 | import li.doerf.hacked.util.NavEvent
19 | import org.joda.time.format.DateTimeFormat
20 |
21 |
22 | class BreachedSitesAdapter(
23 | val context: Context, private var myBreachedSites: List, private val compactView: Boolean) : RecyclerView.Adapter() {
24 |
25 | private lateinit var navEvents: PublishProcessor
26 |
27 | override fun onViewAttachedToWindow(holder: RecyclerViewHolder) {
28 | super.onViewAttachedToWindow(holder)
29 | navEvents = (context as HackedApplication).navEvents
30 | }
31 |
32 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
33 | if (compactView) {
34 | val itemLayout = LayoutInflater.from(parent.context)
35 | .inflate(R.layout.card_breached_site_compact, parent, false)
36 | return RecyclerViewHolder(itemLayout)
37 | }
38 |
39 | val itemLayout = LayoutInflater.from(parent.context)
40 | .inflate(R.layout.card_breached_site, parent, false)
41 | return RecyclerViewHolder(itemLayout)
42 | }
43 |
44 | override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
45 | val site = myBreachedSites[position]
46 | val view = holder.view
47 |
48 | if (compactView) {
49 | bindCompactView(view, site)
50 | } else {
51 | bindFullView(view, site)
52 | }
53 |
54 | }
55 |
56 | private fun bindFullView(siteCard: View, site: BreachedSite) {
57 | val nameView = siteCard.findViewById(R.id.site_name)
58 | nameView.text = site.title
59 |
60 | val pwnCountView = siteCard.findViewById(R.id.pwn_count)
61 | pwnCountView.text = String.format(context.resources.configuration.locale, "(%,d)", site.pwnCount)
62 |
63 | val details = siteCard.findViewById(R.id.breach_details)
64 | val arrowDown = siteCard.findViewById(R.id.arrow_down)
65 | val arrowUp = siteCard.findViewById(R.id.arrow_up)
66 |
67 | if (! site.detailsVisible) {
68 | siteCard.setBackgroundColor(context.resources.getColor(android.R.color.white))
69 | details.visibility = View.GONE
70 | arrowDown.visibility = View.VISIBLE
71 | arrowUp.visibility = View.GONE
72 | } else {
73 | siteCard.setBackgroundColor(context.resources.getColor(R.color.selectedCard))
74 | details.visibility = View.VISIBLE
75 | arrowDown.visibility = View.GONE
76 | arrowUp.visibility = View.VISIBLE
77 | val domain = siteCard.findViewById(R.id.domain)
78 | domain.text = site.domain
79 | val dtfOut = DateTimeFormat.forPattern("yyyy/MM/dd")
80 | val breachDate = siteCard.findViewById(R.id.breach_date)
81 | breachDate.text = dtfOut.print(site.breachDate)
82 | val compromisedData = siteCard.findViewById(R.id.compromised_data)
83 | compromisedData.text = site.dataClasses
84 | val description = siteCard.findViewById(R.id.description)
85 | description.text = Html.fromHtml(site.description).toString()
86 |
87 | val logoView = siteCard.findViewById(R.id.logo)
88 | if (site.logoPath != null && site.logoPath.isNotEmpty()) {
89 | logoView.visibility = View.VISIBLE
90 | Picasso.get().load(site.logoPath).into(logoView)
91 | } else {
92 | logoView.visibility = View.GONE
93 | }
94 | }
95 |
96 | val additionalFlagsLabel: View = siteCard.findViewById(R.id.label_additional_flags)
97 | val additionalFlags: TextView = siteCard.findViewById(R.id.additional_flags)
98 |
99 | if (site.hasAdditionalFlags()) {
100 | additionalFlagsLabel.visibility = View.VISIBLE
101 | additionalFlags.visibility = View.VISIBLE
102 | additionalFlags.setText(getFlags(site))
103 | } else {
104 | additionalFlagsLabel.visibility = View.GONE
105 | additionalFlags.visibility = View.GONE
106 | }
107 |
108 | siteCard.setOnClickListener {
109 | site.detailsVisible = ! site.detailsVisible
110 | notifyDataSetChanged()
111 | }
112 | }
113 |
114 | private fun bindCompactView(card: View, site: BreachedSite) {
115 | val nameView = card.findViewById(R.id.site_name)
116 | nameView.text = site.title
117 |
118 | getFlags(site)
119 |
120 | val pwnCountView = card.findViewById(R.id.pwn_count)
121 | pwnCountView.text = String.format(context.resources.configuration.locale, "(%,d)", site.pwnCount)
122 |
123 | card.setOnClickListener { _ ->
124 | navEvents.onNext(NavEvent(NavEvent.Destination.ALL_BREACHES, site.id, null))
125 | }
126 | }
127 |
128 | private fun getFlags(site: BreachedSite): String? {
129 | val flags = StringBuilder()
130 | if (!site.verified) {
131 | Log.d("BreachedSitesAdapter", "unverified: " + site.name)
132 | flags.append(context.getString(R.string.unverified)).append(" ")
133 | }
134 | if (site.fabricated) {
135 | Log.d("BreachedSitesAdapter", "fabricated: " + site.name)
136 | flags.append(context.getString(R.string.fabricated)).append(" ")
137 | }
138 | if (site.retired) {
139 | Log.d("BreachedSitesAdapter", "retired: " + site.name)
140 | flags.append(context.getString(R.string.retired)).append(" ")
141 | }
142 | if (site.sensitive) {
143 | Log.d("BreachedSitesAdapter", "sensitive: " + site.name)
144 | flags.append(context.getString(R.string.sensitive)).append(" ")
145 | }
146 | if (site.spamList) {
147 | Log.d("BreachedSitesAdapter", "spam_list: " + site.name)
148 | flags.append(context.getString(R.string.spam_list)).append(" ")
149 | }
150 | return flags.toString()
151 | }
152 |
153 | override fun getItemCount(): Int {
154 | return myBreachedSites.size
155 | }
156 |
157 | fun addItems(list: List) {
158 | myBreachedSites = list
159 | notifyDataSetChanged()
160 | }
161 |
162 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/adapters/RecyclerViewHolder.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.adapters
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | /**
7 | * Created by moo on 31/01/15.
8 | */
9 | class RecyclerViewHolder internal constructor(val view: View) : RecyclerView.ViewHolder(view)
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/composable/Breach.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.composable
2 |
3 | import android.content.Context
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material.Text
7 | import androidx.compose.material.TextButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.text.TextStyle
13 | import androidx.compose.ui.text.font.FontWeight
14 | import androidx.compose.ui.unit.dp
15 | import androidx.compose.ui.unit.sp
16 | import androidx.core.text.HtmlCompat
17 | import com.google.accompanist.coil.rememberCoilPainter
18 | import li.doerf.hacked.R
19 | import li.doerf.hacked.db.entities.Breach
20 | import org.joda.time.format.DateTimeFormat
21 |
22 | @Composable
23 | fun BreachUi(breach: Breach, context: Context, handleAcknowledgeClicked: (id: Long) -> Unit) {
24 | val dtfOut = DateTimeFormat.forPattern("yyyy/MM/dd")
25 |
26 | Box(Modifier.padding(8.dp)) {
27 | Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.End) {
28 | Image(
29 | painter = rememberCoilPainter(
30 | request = breach.logoPath
31 | ),
32 | contentDescription = "Logo of ${breach.title}",
33 | modifier = Modifier
34 | .width(48.dp)
35 | .height(48.dp)
36 | )
37 | }
38 | Column() {
39 | Text(breach.title, style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 20.sp))
40 | NameValue(context.getString(R.string.label_domain), breach.domain)
41 | NameValue(context.getString(R.string.label_breach_date), dtfOut.print(breach.breachDate))
42 | NameValue(context.getString(R.string.label_compromised_data), breach.dataClasses, true)
43 | if (breach.hasAdditionalFlags()) {
44 | NameValue(context.getString(R.string.label_additional_flags), getFlags(breach, context))
45 | }
46 | Text(HtmlCompat.fromHtml(breach.description, HtmlCompat.FROM_HTML_MODE_COMPACT).toString())
47 |
48 | if (! breach.acknowledged) {
49 | Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.End) {
50 | TextButton(onClick = { handleAcknowledgeClicked(breach.id) }) {
51 | Text(
52 | context.getString(R.string.acknowledge),
53 | color = Color(context.resources.getColor(R.color.colorAccent))
54 | )
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | @Composable
63 | private fun NameValue(name: String, value: String, valueIsRed: Boolean = false) {
64 | Row() {
65 | Text(name, color = Color.Gray, modifier = Modifier.padding(end = 2.dp))
66 | if (valueIsRed) {
67 | Text(value, color = Color.Red)
68 | } else {
69 | Text(value)
70 | }
71 | }
72 | }
73 |
74 | private fun getFlags(breach: Breach, context: Context): String {
75 | val flags = StringBuilder()
76 | if (!breach.verified) {
77 | flags.append(context.getString(R.string.unverified)).append(" ")
78 | }
79 | if (breach.fabricated) {
80 | flags.append(context.getString(R.string.fabricated)).append(" ")
81 | }
82 | if (breach.retired) {
83 | flags.append(context.getString(R.string.retired)).append(" ")
84 | }
85 | if (breach.sensitive) {
86 | flags.append(context.getString(R.string.sensitive)).append(" ")
87 | }
88 | if (breach.spamList) {
89 | flags.append(context.getString(R.string.spam_list)).append(" ")
90 | }
91 | return flags.toString()
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/fragments/AllBreachesFragment.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.fragments
2 |
3 |
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.os.Bundle
7 | import android.util.Log
8 | import android.view.*
9 | import android.widget.EditText
10 | import androidx.core.widget.addTextChangedListener
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.viewModels
13 | import androidx.lifecycle.Observer
14 | import androidx.navigation.fragment.navArgs
15 | import androidx.recyclerview.widget.LinearLayoutManager
16 | import androidx.recyclerview.widget.RecyclerView
17 | import androidx.work.Constraints
18 | import androidx.work.NetworkType
19 | import androidx.work.OneTimeWorkRequest
20 | import androidx.work.WorkManager
21 | import li.doerf.hacked.R
22 | import li.doerf.hacked.db.entities.BreachedSite
23 | import li.doerf.hacked.remote.hibp.BreachedSitesWorker
24 | import li.doerf.hacked.ui.HibpInfo
25 | import li.doerf.hacked.ui.adapters.BreachedSitesAdapter
26 | import li.doerf.hacked.ui.viewmodels.BreachedSitesViewModel
27 | import java.util.*
28 |
29 | /**
30 | * A simple [Fragment] subclass.
31 | * create an instance of this fragment.
32 | */
33 | class AllBreachesFragment : Fragment() {
34 | private val breachedSitesViewModel: BreachedSitesViewModel by viewModels()
35 | private lateinit var layoutManager: LinearLayoutManager
36 | private var breachedSiteId: Long = -1
37 | private lateinit var breachedSitesAdapter: BreachedSitesAdapter
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | setHasOptionsMenu(true)
42 | val args: AllBreachesFragmentArgs by navArgs()
43 | breachedSiteId = args.breachedSiteId
44 | }
45 |
46 |
47 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
48 | savedInstanceState: Bundle?): View? {
49 | val fragmentRootView = inflater.inflate(R.layout.fragment_all_breaches, container, false)
50 |
51 | val breachedSites: RecyclerView = fragmentRootView.findViewById(R.id.breached_sites_list)
52 | // breachedSites.setHasFixedSize(true)
53 | layoutManager = LinearLayoutManager(context)
54 | breachedSites.layoutManager = layoutManager
55 | breachedSitesAdapter = BreachedSitesAdapter(requireActivity().applicationContext, ArrayList(), false)
56 | breachedSites.adapter = breachedSitesAdapter
57 |
58 | HibpInfo.prepare(context, fragmentRootView.findViewById(R.id.hibp_info), breachedSites)
59 |
60 | val filter = fragmentRootView.findViewById(R.id.filter)
61 | filter.addTextChangedListener { watcher ->
62 | Log.d(LOGTAG, "breaches filter: $watcher")
63 | breachedSitesViewModel.setFilter(watcher.toString())
64 | }
65 |
66 | return fragmentRootView
67 | }
68 |
69 | override fun onAttach(context: Context) {
70 | super.onAttach(context)
71 | breachedSitesViewModel.breachesSites!!.observe(this, Observer { sites: List ->
72 | sites.find { it.id == breachedSiteId }?.detailsVisible = true
73 | breachedSitesAdapter.addItems(sites)
74 | if (breachedSiteId > -1 && sites.isNotEmpty()) {
75 | val position = sites.indexOfFirst { it.id == breachedSiteId }
76 | if (position > -1) {
77 | layoutManager.scrollToPositionWithOffset(position, 0)
78 | }
79 | }
80 | })
81 | }
82 |
83 | override fun onResume() {
84 | super.onResume()
85 | if (breachedSitesAdapter.itemCount == 0 ) {
86 | reloadBreachedSites(requireActivity())
87 | }
88 | }
89 |
90 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
91 | return inflater.inflate(R.menu.menu_fragment_allbreaches, menu)
92 | }
93 |
94 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
95 | if (item.itemId == R.id.sort_by_name) {
96 | breachedSitesViewModel.orderByName()
97 | return true
98 | }
99 | if (item.itemId == R.id.sort_by_count) {
100 | breachedSitesViewModel.orderByCount()
101 | return true
102 | }
103 | if (item.itemId == R.id.sort_by_date) {
104 | breachedSitesViewModel.orderByDate()
105 | return true
106 | }
107 | return super.onOptionsItemSelected(item)
108 | }
109 |
110 | companion object {
111 | private const val LOGTAG = "AllBreachesFragment"
112 | private const val PREF_KEY_LAST_BREACHED_SITES_SYNC = "PREF_KEY_LAST_BREACHED_SITES_SYNC"
113 | private const val SIXHOURS = 6 * 60 * 60 * 1000
114 |
115 | fun reloadBreachedSites(activity: Activity) {
116 | val sharedPref = activity.getPreferences(Context.MODE_PRIVATE) ?: return
117 | val lastSync = sharedPref.getLong(PREF_KEY_LAST_BREACHED_SITES_SYNC, 0)
118 |
119 | if (System.currentTimeMillis() - lastSync > SIXHOURS) {
120 | val constraints = Constraints.Builder()
121 | .setRequiredNetworkType(NetworkType.UNMETERED)
122 | .build()
123 | val checker = OneTimeWorkRequest.Builder(BreachedSitesWorker::class.java)
124 | .setConstraints(constraints)
125 | .build()
126 | WorkManager.getInstance(activity.applicationContext).enqueue(checker)
127 | with (sharedPref.edit()) {
128 | putLong(PREF_KEY_LAST_BREACHED_SITES_SYNC, System.currentTimeMillis())
129 | commit()
130 | }
131 | }
132 | }
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/fragments/BreachesFragment.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.fragments
2 |
3 |
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.ImageView
10 | import androidx.fragment.app.Fragment
11 | import androidx.fragment.app.viewModels
12 | import androidx.lifecycle.Observer
13 | import androidx.recyclerview.widget.LinearLayoutManager
14 | import androidx.recyclerview.widget.RecyclerView
15 | import io.reactivex.processors.PublishProcessor
16 | import li.doerf.hacked.HackedApplication
17 | import li.doerf.hacked.R
18 | import li.doerf.hacked.db.entities.BreachedSite
19 | import li.doerf.hacked.ui.adapters.BreachedSitesAdapter
20 | import li.doerf.hacked.ui.viewmodels.BreachedSitesViewModel
21 | import li.doerf.hacked.util.NavEvent
22 | import java.util.*
23 |
24 | /**
25 | * A simple [Fragment] subclass.
26 | * create an instance of this fragment.
27 | */
28 | class BreachesFragment : Fragment() {
29 | private val breachedSitesViewModel: BreachedSitesViewModel by viewModels()
30 | private lateinit var navEvents: PublishProcessor
31 | private lateinit var breachedSitesAdapter: BreachedSitesAdapter
32 |
33 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
34 | savedInstanceState: Bundle?): View? {
35 | val fragmentRootView = inflater.inflate(R.layout.fragment_breaches, container, false)
36 |
37 | val headingChevron = fragmentRootView.findViewById(R.id.show_details)
38 | headingChevron.setOnClickListener {
39 | navEvents.onNext(NavEvent(NavEvent.Destination.ALL_BREACHES, null, null))
40 | }
41 |
42 | val breachedSites: RecyclerView = fragmentRootView.findViewById(R.id.breached_sites_list)
43 | // breachedSites.setHasFixedSize(true)
44 | val lmbs = LinearLayoutManager(context)
45 | breachedSites.layoutManager = lmbs
46 | breachedSites.adapter = breachedSitesAdapter
47 |
48 | return fragmentRootView
49 | }
50 |
51 | override fun onAttach(context: Context) {
52 | super.onAttach(context)
53 | breachedSitesAdapter = BreachedSitesAdapter(requireActivity().applicationContext, ArrayList(), true)
54 | breachedSitesViewModel.breachesSitesMostRecent!!.observe(this, Observer { sites: List -> breachedSitesAdapter.addItems(sites) })
55 | navEvents = (requireActivity().applicationContext as HackedApplication).navEvents
56 | }
57 |
58 | override fun onResume() {
59 | super.onResume()
60 | if (breachedSitesAdapter.itemCount == 0 ) {
61 | AllBreachesFragment.reloadBreachedSites(requireActivity())
62 | }
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/li/doerf/hacked/ui/fragments/FirstUseFragment.kt:
--------------------------------------------------------------------------------
1 | package li.doerf.hacked.ui.fragments
2 |
3 |
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.text.method.LinkMovementMethod
7 | import android.view.KeyEvent
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.view.inputmethod.EditorInfo
12 | import android.widget.Button
13 | import android.widget.EditText
14 | import android.widget.TextView
15 | import androidx.fragment.app.Fragment
16 | import io.reactivex.processors.PublishProcessor
17 | import li.doerf.hacked.CustomEvent
18 | import li.doerf.hacked.HackedApplication
19 | import li.doerf.hacked.R
20 | import li.doerf.hacked.services.AccountService
21 | import li.doerf.hacked.util.Analytics
22 | import li.doerf.hacked.util.NavEvent
23 |
24 | /**
25 | * A simple [Fragment] subclass.
26 | */
27 | class FirstUseFragment : Fragment() {
28 |
29 | private lateinit var navEvents: PublishProcessor
30 |
31 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
32 | savedInstanceState: Bundle?): View? {
33 | // Inflate the layout for this fragment
34 | val fragmentRootView = inflater.inflate(R.layout.fragment_first_use, container, false)
35 |
36 | val accountEditText = fragmentRootView.findViewById(R.id.account)
37 | val addButton = fragmentRootView.findViewById