├── .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 | 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 | 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 44 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 76 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/navEditor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 137 | 138 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/doerfli/hacked/workflows/Build/badge.svg) 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