├── .github
└── workflows
│ ├── android_ci.yml
│ └── ci-release-artifacts.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── icon.png
├── icon_dark.png
├── inspectionProfiles
│ └── Project_Default.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── schemas
│ └── dev.sasikanth.pinnit.data.AppDatabase
│ │ ├── 1.json
│ │ └── 2.json
└── src
│ ├── androidTest
│ └── java
│ │ └── dev
│ │ └── sasikanth
│ │ └── pinnit
│ │ ├── AndroidTestRunner.kt
│ │ ├── CanaryAndroidTest.kt
│ │ ├── SqliteHelperFunctions.kt
│ │ ├── data
│ │ └── migrations
│ │ │ └── Migration2Test.kt
│ │ ├── di
│ │ └── TestAppModule.kt
│ │ └── notifications
│ │ └── NotificationsRepositoryAndroidTest.kt
│ ├── debug
│ └── res
│ │ ├── values-night
│ │ └── floats.xml
│ │ ├── values
│ │ └── strings.xml
│ │ └── xml-v25
│ │ └── shortcuts.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── dev
│ │ │ └── sasikanth
│ │ │ └── pinnit
│ │ │ ├── AppActionsReceiverActivity.kt
│ │ │ ├── PinnitApp.kt
│ │ │ ├── ShortcutReceiverActivity.kt
│ │ │ ├── SplashActivity.kt
│ │ │ ├── about
│ │ │ └── AboutBottomSheet.kt
│ │ │ ├── activity
│ │ │ └── MainActivity.kt
│ │ │ ├── background
│ │ │ ├── receivers
│ │ │ │ ├── AppUpdateReceiver.kt
│ │ │ │ ├── BootCompletedReceiver.kt
│ │ │ │ ├── DeleteNotificationReceiver.kt
│ │ │ │ └── UnpinNotificationReceiver.kt
│ │ │ └── services
│ │ │ │ └── NotificationsListener.kt
│ │ │ ├── data
│ │ │ ├── AppDatabase.kt
│ │ │ ├── PinnitNotification.kt
│ │ │ ├── Schedule.kt
│ │ │ ├── migrations
│ │ │ │ └── Migration_1_2.kt
│ │ │ └── preferences
│ │ │ │ ├── AppPreferencesDataStore.kt
│ │ │ │ └── AppPreferencesSerializer.kt
│ │ │ ├── di
│ │ │ ├── AppModule.kt
│ │ │ ├── DateTimeFormat.kt
│ │ │ ├── DateTimeFormatterModule.kt
│ │ │ ├── PreferencesModule.kt
│ │ │ └── WorkManagerModule.kt
│ │ │ ├── editor
│ │ │ ├── CurrentDateValidator.kt
│ │ │ ├── EditorScreen.kt
│ │ │ ├── EditorScreenEffect.kt
│ │ │ ├── EditorScreenEffectHandler.kt
│ │ │ ├── EditorScreenEvent.kt
│ │ │ ├── EditorScreenInit.kt
│ │ │ ├── EditorScreenModel.kt
│ │ │ ├── EditorScreenUi.kt
│ │ │ ├── EditorScreenUiRender.kt
│ │ │ ├── EditorScreenUpdate.kt
│ │ │ ├── EditorScreenViewEffect.kt
│ │ │ ├── EditorScreenViewModel.kt
│ │ │ ├── EditorTransition.kt
│ │ │ └── ScheduleValidator.kt
│ │ │ ├── notifications
│ │ │ ├── NotificationModule.kt
│ │ │ ├── NotificationRepository.kt
│ │ │ ├── NotificationScreenViewEffect.kt
│ │ │ ├── NotificationsScreen.kt
│ │ │ ├── NotificationsScreenEffect.kt
│ │ │ ├── NotificationsScreenEffectHandler.kt
│ │ │ ├── NotificationsScreenEvent.kt
│ │ │ ├── NotificationsScreenInit.kt
│ │ │ ├── NotificationsScreenModel.kt
│ │ │ ├── NotificationsScreenUiRender.kt
│ │ │ ├── NotificationsScreenUpdate.kt
│ │ │ ├── NotificationsScreenViewModel.kt
│ │ │ └── adapter
│ │ │ │ ├── NotificationPinItemAnimator.kt
│ │ │ │ ├── NotificationsDiffCallback.kt
│ │ │ │ ├── NotificationsItemTouchHelper.kt
│ │ │ │ └── NotificationsListAdapter.kt
│ │ │ ├── oemwarning
│ │ │ ├── OemChecker.kt
│ │ │ └── OemWarningDialog.kt
│ │ │ ├── options
│ │ │ └── OptionsBottomSheet.kt
│ │ │ ├── scheduler
│ │ │ └── PinnitNotificationScheduler.kt
│ │ │ ├── utils
│ │ │ ├── ContextExt.kt
│ │ │ ├── DatabaseExt.kt
│ │ │ ├── DispatcherProvider.kt
│ │ │ ├── DrawableExt.kt
│ │ │ ├── Globals.kt
│ │ │ ├── UserClock.kt
│ │ │ ├── UtcClock.kt
│ │ │ ├── ViewExt.kt
│ │ │ ├── notification
│ │ │ │ └── NotificationUtil.kt
│ │ │ └── room
│ │ │ │ ├── JavaTimeRoomTypeConverters.kt
│ │ │ │ └── UuidRoomTypeConverter.kt
│ │ │ ├── widgets
│ │ │ ├── CheckableImageView.kt
│ │ │ └── PinnitBottomBar.kt
│ │ │ └── worker
│ │ │ └── ScheduleWorker.kt
│ ├── proto
│ │ └── app_preferences.proto
│ └── res
│ │ ├── anim
│ │ ├── pinnit_bottom_sheet_slide_in.xml
│ │ └── pinnit_bottom_sheet_slide_out.xml
│ │ ├── color
│ │ ├── about_item.xml
│ │ ├── button_group_background_state.xml
│ │ ├── button_group_stroke_state.xml
│ │ ├── button_group_text_state.xml
│ │ ├── color_notification_divider.xml
│ │ ├── color_on_background_15.xml
│ │ ├── color_on_background_70.xml
│ │ ├── color_secondary_20.xml
│ │ ├── color_secondary_5.xml
│ │ ├── edit_text_stroke_color.xml
│ │ ├── material_button_background_tint.xml
│ │ ├── material_button_text_color.xml
│ │ ├── schedule_checkbox_state.xml
│ │ ├── schedule_indicator_icon_state.xml
│ │ ├── schedule_indicator_stroke_state.xml
│ │ └── schedule_indicator_text_state.xml
│ │ ├── drawable-nodpi
│ │ └── pinnit_icon.png
│ │ ├── drawable-v26
│ │ └── ic_shortcut_create.xml
│ │ ├── drawable
│ │ ├── about_item_ripple.xml
│ │ ├── asld_pin_unpin.xml
│ │ ├── avd_add_to_delete.xml
│ │ ├── avd_pin_to_pinned.xml
│ │ ├── avd_pin_welcome.xml
│ │ ├── avd_pinned_to_unpinned.xml
│ │ ├── bottom_bar_item_ripple.xml
│ │ ├── divider.xml
│ │ ├── ic_arrow_back.xml
│ │ ├── ic_arrow_drop_down.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_launcher_foreground_themed.xml
│ │ ├── ic_pinnit_about.xml
│ │ ├── ic_pinnit_add.xml
│ │ ├── ic_pinnit_app_icon.xml
│ │ ├── ic_pinnit_dark_mode.xml
│ │ ├── ic_pinnit_date.xml
│ │ ├── ic_pinnit_delete.xml
│ │ ├── ic_pinnit_email.xml
│ │ ├── ic_pinnit_notification.xml
│ │ ├── ic_pinnit_pin.xml
│ │ ├── ic_pinnit_pinned.xml
│ │ ├── ic_pinnit_source_code.xml
│ │ ├── ic_pinnit_warning.xml
│ │ ├── ic_pinnit_warning_80dp.xml
│ │ ├── ic_qs_app_icon.xml
│ │ ├── ic_shortcut_create.xml
│ │ ├── ic_shortcut_create_background.xml
│ │ ├── ic_shortcut_create_foreground.xml
│ │ ├── illustration_pinnit_no_notifications.xml
│ │ ├── illustration_pinnit_pin.xml
│ │ ├── notification_divider.xml
│ │ ├── rectangle.xml
│ │ └── splash_layout.xml
│ │ ├── font
│ │ ├── bai_jam_bold.ttf
│ │ ├── bai_jam_medium.ttf
│ │ ├── bai_jam_regular.ttf
│ │ ├── bai_jam_semibold.ttf
│ │ ├── jura_bold.xml
│ │ └── jura_semibold.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_splash.xml
│ │ ├── fragment_notification_editor.xml
│ │ ├── fragment_notifications.xml
│ │ ├── notifications_list_item.xml
│ │ ├── pinnit_bottom_bar.xml
│ │ ├── pinnit_oem_warning_dialog.xml
│ │ ├── sheet_about.xml
│ │ ├── theme_selection_sheet.xml
│ │ └── view_schedule.xml
│ │ ├── menu
│ │ └── notification_schedule.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── main_nav_graph.xml
│ │ ├── values-es
│ │ └── strings-es.xml
│ │ ├── values-land
│ │ └── dimens.xml
│ │ ├── values-night
│ │ ├── colors.xml
│ │ └── sys_ui.xml
│ │ ├── values-pt-rBR
│ │ └── strings.xml
│ │ ├── values-v27
│ │ ├── styles.xml
│ │ └── theme.xml
│ │ ├── values
│ │ ├── arrays.xml
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── floats.xml
│ │ ├── font_certs.xml
│ │ ├── pinnit_bottom_bar.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ ├── styles_typography.xml
│ │ ├── sys_ui.xml
│ │ └── theme.xml
│ │ ├── xml-v25
│ │ └── shortcuts.xml
│ │ └── xml
│ │ └── actions.xml
│ └── test
│ ├── java
│ └── dev
│ │ └── sasikanth
│ │ └── pinnit
│ │ ├── CanaryTest.kt
│ │ ├── editor
│ │ ├── EditorScreenEffectHandlerTest.kt
│ │ ├── EditorScreenInitTest.kt
│ │ ├── EditorScreenUiRenderTest.kt
│ │ ├── EditorScreenUpdateTest.kt
│ │ └── ScheduleValidatorTest.kt
│ │ ├── notifications
│ │ ├── NotificationsScreenEffectHandlerTest.kt
│ │ ├── NotificationsScreenInitTest.kt
│ │ ├── NotificationsScreenUiRenderTest.kt
│ │ └── NotificationsScreenUpdateTest.kt
│ │ ├── oemwarning
│ │ └── OemCheckerTest.kt
│ │ └── utils
│ │ └── TestDispatcherProvider.kt
│ └── resources
│ └── mockito-extensions
│ └── org.mockito.plugins.MockMaker
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── icon.png
├── release
└── app-release.gpg
├── renovate.json
├── settings.gradle.kts
└── sharedTestCode
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
└── main
├── AndroidManifest.xml
└── java
└── dev
└── sasikanth
└── sharedtestcode
├── TestData.kt
└── utils
├── TestUserClock.kt
└── TestUtcClock.kt
/.github/workflows/android_ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ trunk ]
6 | pull_request:
7 | branches: [ trunk ]
8 |
9 | jobs:
10 | unit-tests:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: set up JDK 17
16 | uses: actions/setup-java@v4
17 | with:
18 | java-version: 17
19 | distribution: zulu
20 | cache: 'gradle'
21 |
22 | - name: Unit Tests
23 | run: ./gradlew testDebugUnitTest
24 |
25 | android-tests:
26 | runs-on: ubuntu-latest
27 | timeout-minutes: 30
28 | steps:
29 | - uses: actions/checkout@v4
30 |
31 | - name: Enable KVM group perms
32 | run: |
33 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
34 | sudo udevadm control --reload-rules
35 | sudo udevadm trigger --name-match=kvm
36 |
37 | - name: set up JDK 17
38 | uses: actions/setup-java@v4
39 | with:
40 | java-version: 17
41 | distribution: zulu
42 | cache: 'gradle'
43 |
44 | - name: Android Tests
45 | uses: reactivecircus/android-emulator-runner@v2
46 | with:
47 | api-level: 31
48 | arch: x86_64
49 | disable-animations: true
50 | script: ./gradlew connectedDebugAndroidTest
51 |
--------------------------------------------------------------------------------
/.github/workflows/ci-release-artifacts.yml:
--------------------------------------------------------------------------------
1 | name: Android Play Store release builds
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | tramline-input:
7 | description: "Tramline input"
8 | required: false
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | env:
14 | TERM: dumb
15 | ORG_GRADLE_PROJECT_PINNIT_KEYSTORE_PASSWORD: ${{ secrets.PINNIT_KEYSTORE_PASSWORD }}
16 | ORG_GRADLE_PROJECT_PINNIT_KEY_PASSWORD: ${{ secrets.PINNIT_KEY_PASSWORD }}
17 | steps:
18 | - name: Configure Tramline
19 | id: tramline
20 | uses: tramlinehq/deploy-action@v0.1.6
21 | with:
22 | input: ${{ github.event.inputs.tramline-input }}
23 |
24 | - name: Setup versionName regardless of how this action is triggered
25 | id: version_name
26 | run: |
27 | WORKFLOW_INPUT=${{ steps.tramline.outputs.version_name }}
28 | VERSION_NAME=${WORKFLOW_INPUT:-"1.0.0"}
29 | echo "ORG_GRADLE_PROJECT_VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
30 |
31 | - name: Setup versionCode regardless of how this action is triggered
32 | id: version_code
33 | run: |
34 | WORKFLOW_INPUT=${{ steps.tramline.outputs.version_code }}
35 | VERSION_CODE=${WORKFLOW_INPUT:-"1"}
36 | echo "ORG_GRADLE_PROJECT_VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
37 |
38 | - uses: actions/setup-java@v4
39 | with:
40 | java-version: '17'
41 | distribution: zulu
42 | cache: 'gradle'
43 |
44 | - name: Decrypt secrets
45 | run: gpg --batch --yes --quiet --decrypt --passphrase=${{ secrets.ENCRYPT_KEY }} --output release/app-release.jks release/app-release.gpg
46 |
47 | - name: Build release artifact
48 | run: ./gradlew bundle
49 |
50 | - name: Upload Release Bundle
51 | uses: actions/upload-artifact@v4
52 | with:
53 | name: release-aab
54 | path: app/build/outputs/bundle/release/app-release.aab
55 |
56 | - name: Clean secrets
57 | run: |
58 | rm -f release/app-release.jks
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | .idea/jarRepositories.xml
48 | # Android Studio 3 in .gitignore file.
49 | .idea/caches
50 | .idea/modules.xml
51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
52 | .idea/navEditor.xml
53 | .idea/markdown-navigator.xml
54 | .idea/markdown-navigator-enh.xml
55 | .idea/misc.xml
56 | .idea/compiler.xml
57 | .idea/deploymentTargetDropDown.xml
58 | .idea/kotlinc.xml
59 | .kotlin
60 |
61 | # Keystore files
62 | # Uncomment the following lines if you do not want to check your keystore files in.
63 | #*.jks
64 | #*.keystore
65 |
66 | # External native build folder generated in Android Studio 2.2 and later
67 | .externalNativeBuild
68 | .cxx/
69 |
70 | # Google Services (e.g. APIs or Firebase)
71 | # google-services.json
72 |
73 | # Freeline
74 | freeline.py
75 | freeline/
76 | freeline_project_description.json
77 |
78 | # fastlane
79 | fastlane/report.xml
80 | fastlane/Preview.html
81 | fastlane/screenshots
82 | fastlane/test_output
83 | fastlane/readme.md
84 |
85 | # Version control
86 | vcs.xml
87 |
88 | # lint
89 | lint/intermediates/
90 | lint/generated/
91 | lint/outputs/
92 | lint/tmp/
93 | # lint/reports/
94 |
95 | # Android Profiling
96 | *.hprof
97 |
98 | *.DS_Store
99 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/.idea/icon.png
--------------------------------------------------------------------------------
/.idea/icon_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/.idea/icon_dark.png
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pinnit
2 |
3 | Notification history and pinning
4 |
5 | [](https://github.com/msasikanth/pinnit/actions/workflows/android_ci.yml)
6 |
7 | ## Development
8 | Pinnit is written entirely in Kotlin & uses latest Android libraries like
9 | - [Architecture Components](https://developer.android.com/topic/libraries/architecture) (Room, Navigation & Lifecycle)
10 | - [Coroutines](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html)
11 | - [Dagger](https://dagger.dev) for DI
12 | - [Mobius](https://github.com/spotify/mobius/wiki) for state management
13 |
14 | ## Setup
15 | Requires [Android Studio](https://developer.android.com/studio/) 4.0.0 or above
16 |
17 | ### Code style
18 | Project already has a code style. You can find the XML to import [here](https://github.com/msasikanth/pinnit/blob/master/.idea/codeStyles/Project.xml)
19 |
20 | ## Links
21 |
22 |
23 | ## Contributors
24 | - [Sasikanth](https://twitter.com/its_sasikanth) - Developer
25 | - [Eduardo Pratti](https://twitter.com/edpratti) - Designer
26 |
27 | ## License
28 | ```
29 | Copyright 2020 Sasikanth Miriyampalli
30 |
31 | Licensed under the Apache License, Version 2.0 (the "License");
32 | you may not use this file except in compliance with the License.
33 | You may obtain a copy of the License at
34 |
35 | https://www.apache.org/licenses/LICENSE-2.0
36 |
37 | Unless required by applicable law or agreed to in writing, software
38 | distributed under the License is distributed on an "AS IS" BASIS,
39 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
40 | See the License for the specific language governing permissions and
41 | limitations under the License.
42 | ```
43 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/schemas/dev.sasikanth.pinnit.data.AppDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "3c8eb899aafaefb747668e4561b87421",
6 | "entities": [
7 | {
8 | "tableName": "PinnitNotification",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT, `isPinned` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `updatedAt` TEXT NOT NULL, `deletedAt` TEXT, PRIMARY KEY(`uuid`))",
10 | "fields": [
11 | {
12 | "fieldPath": "uuid",
13 | "columnName": "uuid",
14 | "affinity": "TEXT",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "title",
19 | "columnName": "title",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "content",
25 | "columnName": "content",
26 | "affinity": "TEXT",
27 | "notNull": false
28 | },
29 | {
30 | "fieldPath": "isPinned",
31 | "columnName": "isPinned",
32 | "affinity": "INTEGER",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "createdAt",
37 | "columnName": "createdAt",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "updatedAt",
43 | "columnName": "updatedAt",
44 | "affinity": "TEXT",
45 | "notNull": true
46 | },
47 | {
48 | "fieldPath": "deletedAt",
49 | "columnName": "deletedAt",
50 | "affinity": "TEXT",
51 | "notNull": false
52 | }
53 | ],
54 | "primaryKey": {
55 | "columnNames": [
56 | "uuid"
57 | ],
58 | "autoGenerate": false
59 | },
60 | "indices": [],
61 | "foreignKeys": []
62 | }
63 | ],
64 | "views": [],
65 | "setupQueries": [
66 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
67 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3c8eb899aafaefb747668e4561b87421')"
68 | ]
69 | }
70 | }
--------------------------------------------------------------------------------
/app/schemas/dev.sasikanth.pinnit.data.AppDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "c975165bcd52149982982bac0ecc22b6",
6 | "entities": [
7 | {
8 | "tableName": "PinnitNotification",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT, `isPinned` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `updatedAt` TEXT NOT NULL, `deletedAt` TEXT, `scheduleDate` TEXT, `scheduleTime` TEXT, `scheduleType` TEXT, PRIMARY KEY(`uuid`))",
10 | "fields": [
11 | {
12 | "fieldPath": "uuid",
13 | "columnName": "uuid",
14 | "affinity": "TEXT",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "title",
19 | "columnName": "title",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "content",
25 | "columnName": "content",
26 | "affinity": "TEXT",
27 | "notNull": false
28 | },
29 | {
30 | "fieldPath": "isPinned",
31 | "columnName": "isPinned",
32 | "affinity": "INTEGER",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "createdAt",
37 | "columnName": "createdAt",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "updatedAt",
43 | "columnName": "updatedAt",
44 | "affinity": "TEXT",
45 | "notNull": true
46 | },
47 | {
48 | "fieldPath": "deletedAt",
49 | "columnName": "deletedAt",
50 | "affinity": "TEXT",
51 | "notNull": false
52 | },
53 | {
54 | "fieldPath": "schedule.scheduleDate",
55 | "columnName": "scheduleDate",
56 | "affinity": "TEXT",
57 | "notNull": false
58 | },
59 | {
60 | "fieldPath": "schedule.scheduleTime",
61 | "columnName": "scheduleTime",
62 | "affinity": "TEXT",
63 | "notNull": false
64 | },
65 | {
66 | "fieldPath": "schedule.scheduleType",
67 | "columnName": "scheduleType",
68 | "affinity": "TEXT",
69 | "notNull": false
70 | }
71 | ],
72 | "primaryKey": {
73 | "columnNames": [
74 | "uuid"
75 | ],
76 | "autoGenerate": false
77 | },
78 | "indices": [],
79 | "foreignKeys": []
80 | }
81 | ],
82 | "views": [],
83 | "setupQueries": [
84 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
85 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c975165bcd52149982982bac0ecc22b6')"
86 | ]
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/sasikanth/pinnit/AndroidTestRunner.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.test.runner.AndroidJUnitRunner
6 | import dagger.hilt.android.testing.HiltTestApplication
7 |
8 | class AndroidTestRunner : AndroidJUnitRunner() {
9 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
10 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/sasikanth/pinnit/CanaryAndroidTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.google.common.truth.Truth.assertThat
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | @RunWith(AndroidJUnit4::class)
10 | class CanaryAndroidTest {
11 |
12 | @Test
13 | fun testEnvWorks() {
14 | val context = InstrumentationRegistry.getInstrumentation().targetContext
15 | assertThat(context).isNotNull()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/sasikanth/pinnit/SqliteHelperFunctions.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.content.ContentValues
4 | import android.database.Cursor
5 | import android.database.sqlite.SQLiteDatabase
6 | import androidx.sqlite.db.SupportSQLiteDatabase
7 | import com.google.common.truth.Truth
8 | import dev.sasikanth.pinnit.utils.room.LocalDateRoomTypeConverter
9 | import java.time.Instant
10 | import java.time.LocalDate
11 | import java.util.UUID
12 |
13 | private val dateConverter = LocalDateRoomTypeConverter()
14 |
15 | fun Cursor.string(column: String): String? = getString(getColumnIndex(column))
16 | fun Cursor.boolean(column: String): Boolean? = getInt(getColumnIndex(column)) == 1
17 | fun Cursor.integer(columnName: String): Int? = getInt(getColumnIndex(columnName))
18 | fun Cursor.long(columnName: String): Long = getLong(getColumnIndex(columnName))
19 | fun Cursor.double(columnName: String): Double = getDouble(getColumnIndex(columnName))
20 | fun Cursor.float(columnName: String): Float = getFloat(getColumnIndex(columnName))
21 | fun Cursor.uuid(columnName: String): UUID? = string(columnName)?.let { UUID.fromString(it) }
22 | fun Cursor.instant(columnName: String): Instant? = string(columnName)?.let { Instant.parse(it) }
23 | fun Cursor.localDate(columnName: String): LocalDate? = string(columnName).let(dateConverter::toLocalDate)
24 |
25 | fun SupportSQLiteDatabase.insert(tableName: String, valuesMap: Map) {
26 | val contentValues = valuesMap
27 | .entries
28 | .fold(ContentValues()) { values, (key, value) ->
29 | when (value) {
30 | null -> values.putNull(key)
31 | is Int -> values.put(key, value)
32 | is Long -> values.put(key, value)
33 | is Float -> values.put(key, value)
34 | is Double -> values.put(key, value)
35 | is Boolean -> values.put(key, value)
36 | is String -> values.put(key, value)
37 | is UUID -> values.put(key, value.toString())
38 | is Instant -> values.put(key, value.toString())
39 | is LocalDate -> values.put(key, dateConverter.fromLocalDate(value))
40 | else -> throw IllegalArgumentException("Unknown type (${value.javaClass.name}) for key: $key")
41 | }
42 |
43 | values
44 | }
45 |
46 | insert(tableName, SQLiteDatabase.CONFLICT_ABORT, contentValues)
47 | }
48 |
49 | fun Cursor.assertValues(valuesMap: Map) {
50 | Truth.assertThat(columnNames.toSet()).containsExactlyElementsIn(valuesMap.keys)
51 | valuesMap
52 | .forEach { (key, value) ->
53 | val withMessage = Truth.assertWithMessage("For column [$key]: ")
54 | when (value) {
55 | null -> withMessage.that(isNull(getColumnIndex(key))).isTrue()
56 | is Int -> withMessage.that(integer(key)).isEqualTo(value)
57 | is Long -> withMessage.that(long(key)).isEqualTo(value)
58 | is Float -> withMessage.that(float(key)).isEqualTo(value)
59 | is Double -> withMessage.that(double(key)).isEqualTo(value)
60 | is Boolean -> withMessage.that(boolean(key)).isEqualTo(value)
61 | is String -> withMessage.that(string(key)).isEqualTo(value)
62 | is UUID -> withMessage.that(uuid(key)).isEqualTo(value)
63 | is Instant -> withMessage.that(instant(key)).isEqualTo(value)
64 | is LocalDate -> withMessage.that(localDate(key)).isEqualTo(value)
65 | else -> throw IllegalArgumentException("Unknown type (${value.javaClass.name}) for key: $key")
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/sasikanth/pinnit/data/migrations/Migration2Test.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data.migrations
2 |
3 | import androidx.room.testing.MigrationTestHelper
4 | import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import dev.sasikanth.pinnit.assertValues
8 | import dev.sasikanth.pinnit.data.AppDatabase
9 | import dev.sasikanth.pinnit.insert
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.junit.runner.RunWith
13 | import java.io.IOException
14 | import java.time.Instant
15 | import java.util.UUID
16 |
17 | private const val TEST_DB = "pinnit-test-db"
18 |
19 | @RunWith(AndroidJUnit4::class)
20 | class Migration2Test {
21 |
22 | @get:Rule
23 | val helper: MigrationTestHelper = MigrationTestHelper(
24 | InstrumentationRegistry.getInstrumentation(),
25 | AppDatabase::class.java,
26 | specs = emptyList(),
27 | FrameworkSQLiteOpenHelperFactory()
28 | )
29 |
30 | @Test
31 | @Throws(IOException::class)
32 | fun migrate1To2() {
33 | // given
34 | val notificationId = UUID.fromString("4179c32e-3483-4d45-962d-0e58d3b4d960")
35 | var db = helper.createDatabase(TEST_DB, 1)
36 |
37 | db.insert(
38 | "PinnitNotification", mapOf(
39 | "uuid" to notificationId,
40 | "title" to "Sample Title",
41 | "content" to "Sample Content",
42 | "isPinned" to true,
43 | "createdAt" to Instant.parse("2020-01-01T00:00:00.00Z"),
44 | "updatedAt" to Instant.parse("2020-01-01T00:00:00.00Z"),
45 | "deletedAt" to null
46 | )
47 | )
48 |
49 | // Prepare for the next version.
50 | db.close()
51 |
52 | // when
53 | db = helper.runMigrationsAndValidate(TEST_DB, 2, true, Migration_1_2)
54 |
55 | // then
56 | db.query(""" SELECT * FROM PinnitNotification """).use { cursor ->
57 | cursor.moveToFirst()
58 | cursor.assertValues(
59 | mapOf(
60 | "uuid" to notificationId,
61 | "title" to "Sample Title",
62 | "content" to "Sample Content",
63 | "isPinned" to true,
64 | "createdAt" to Instant.parse("2020-01-01T00:00:00.00Z"),
65 | "updatedAt" to Instant.parse("2020-01-01T00:00:00.00Z"),
66 | "deletedAt" to null,
67 | "scheduleDate" to null,
68 | "scheduleTime" to null,
69 | "scheduleType" to null
70 | )
71 | )
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/sasikanth/pinnit/di/TestAppModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.spotify.mobius.runners.ImmediateWorkRunner
6 | import com.spotify.mobius.runners.WorkRunner
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import dagger.hilt.testing.TestInstallIn
12 | import dev.sasikanth.pinnit.data.AppDatabase
13 | import dev.sasikanth.pinnit.utils.CoroutineDispatcherProvider
14 | import dev.sasikanth.pinnit.utils.DispatcherProvider
15 | import dev.sasikanth.pinnit.utils.UserClock
16 | import dev.sasikanth.pinnit.utils.UtcClock
17 | import dev.sasikanth.sharedtestcode.utils.TestUserClock
18 | import dev.sasikanth.sharedtestcode.utils.TestUtcClock
19 | import java.time.ZoneId
20 | import javax.inject.Singleton
21 |
22 | @TestInstallIn(
23 | components = [SingletonComponent::class],
24 | replaces = [AppModule::class]
25 | )
26 | @Module
27 | object TestAppModule {
28 |
29 | @Singleton
30 | @Provides
31 | fun providesTestAppDatabase(
32 | @ApplicationContext context: Context
33 | ): AppDatabase {
34 | return Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
35 | .build()
36 | }
37 |
38 | @Singleton
39 | @Provides
40 | fun providesTestUtcClock(): TestUtcClock = TestUtcClock()
41 |
42 | @Singleton
43 | @Provides
44 | fun providesUtcClock(testUtcClock: TestUtcClock): UtcClock = testUtcClock
45 |
46 | @Singleton
47 | @Provides
48 | fun testUserClock(): TestUserClock {
49 | return TestUserClock()
50 | }
51 |
52 | @Singleton
53 | @Provides
54 | fun userClock(testUserClock: TestUserClock): UserClock = testUserClock
55 |
56 | @Singleton
57 | @Provides
58 | fun providesDispatcherProvider(): DispatcherProvider = CoroutineDispatcherProvider()
59 |
60 | @Singleton
61 | @Provides
62 | fun providesSystemDefaultZone(): ZoneId = ZoneId.systemDefault()
63 |
64 | @Singleton
65 | @Provides
66 | fun providesWorkRunner(): WorkRunner = ImmediateWorkRunner()
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/debug/res/values-night/floats.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - 0.8
4 |
5 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Pinnit (D)
3 |
4 |
--------------------------------------------------------------------------------
/app/src/debug/res/xml-v25/shortcuts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/AppActionsReceiverActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.navigation.NavDeepLinkBuilder
6 | import dev.sasikanth.pinnit.activity.MainActivity
7 | import dev.sasikanth.pinnit.editor.EditorScreenArgs
8 | import dev.sasikanth.pinnit.editor.EditorTransition
9 |
10 | class AppActionsReceiverActivity : AppCompatActivity() {
11 |
12 | companion object {
13 | const val MESSAGE = "message"
14 | }
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 |
19 | val receivedText = intent?.data?.getQueryParameter(MESSAGE)
20 | val editorScreenArgs = EditorScreenArgs(
21 | notificationTitle = receivedText,
22 | editorTransition = EditorTransition.SharedAxis
23 | ).toBundle()
24 |
25 | // Launch the deep link to editor page
26 | NavDeepLinkBuilder(this)
27 | .setComponentName(MainActivity::class.java)
28 | .setDestination(R.id.editorScreen)
29 | .setGraph(R.navigation.main_nav_graph)
30 | .setArguments(editorScreenArgs)
31 | .createTaskStackBuilder()
32 | .startActivities()
33 |
34 | // finish the activity once necessary actions are performed
35 | finish()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/PinnitApp.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.app.Application
4 | import android.os.StrictMode
5 | import android.os.StrictMode.ThreadPolicy
6 | import android.os.StrictMode.VmPolicy
7 | import androidx.appcompat.app.AppCompatDelegate
8 | import androidx.datastore.core.DataStore
9 | import androidx.work.Configuration
10 | import dagger.hilt.android.HiltAndroidApp
11 | import dev.sasikanth.pinnit.data.preferences.AppPreferences
12 | import dev.sasikanth.pinnit.utils.DispatcherProvider
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.flow.launchIn
15 | import kotlinx.coroutines.flow.map
16 | import kotlinx.coroutines.flow.onEach
17 | import javax.inject.Inject
18 |
19 |
20 | @HiltAndroidApp
21 | class PinnitApp : Application(), Configuration.Provider {
22 |
23 | @Inject
24 | lateinit var appPreferencesStore: DataStore
25 |
26 | @Inject
27 | lateinit var configuration: Configuration
28 |
29 | @Inject
30 | lateinit var dispatcherProvider: DispatcherProvider
31 |
32 | private val mainScope by lazy {
33 | CoroutineScope(dispatcherProvider.main)
34 | }
35 |
36 | override val workManagerConfiguration: Configuration
37 | get() = configuration
38 |
39 | override fun onCreate() {
40 | if (BuildConfig.DEBUG) {
41 | StrictMode.setThreadPolicy(
42 | ThreadPolicy.Builder()
43 | .detectDiskReads()
44 | .detectDiskWrites()
45 | .penaltyLog()
46 | .build()
47 | )
48 | StrictMode.setVmPolicy(
49 | VmPolicy.Builder()
50 | .detectLeakedSqlLiteObjects()
51 | .detectLeakedClosableObjects()
52 | .penaltyLog()
53 | .penaltyDeath()
54 | .build()
55 | )
56 | }
57 |
58 | super.onCreate()
59 |
60 | appPreferencesStore
61 | .data
62 | .map { it.theme }
63 | .onEach(::setAppTheme)
64 | .launchIn(mainScope)
65 | }
66 |
67 | private fun setAppTheme(theme: AppPreferences.Theme) = when (theme) {
68 | AppPreferences.Theme.DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
69 | AppPreferences.Theme.LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
70 | AppPreferences.Theme.AUTO -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
71 | else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/ShortcutReceiverActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.content.Intent.EXTRA_TEXT
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.navigation.NavDeepLinkBuilder
7 | import dev.sasikanth.pinnit.activity.MainActivity
8 | import dev.sasikanth.pinnit.editor.EditorScreenArgs
9 | import dev.sasikanth.pinnit.editor.EditorTransition.SharedAxis
10 |
11 | class ShortcutReceiverActivity : AppCompatActivity() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 |
16 | val receivedText = intent.getStringExtra(EXTRA_TEXT)
17 | val editorScreenArgs = EditorScreenArgs(
18 | notificationContent = receivedText,
19 | editorTransition = SharedAxis
20 | ).toBundle()
21 |
22 | // Launch the deep link to editor page
23 | NavDeepLinkBuilder(this)
24 | .setComponentName(MainActivity::class.java)
25 | .setDestination(R.id.editorScreen)
26 | .setGraph(R.navigation.main_nav_graph)
27 | .setArguments(editorScreenArgs)
28 | .createTaskStackBuilder()
29 | .startActivities()
30 |
31 | // finish the activity once necessary actions are performed
32 | finish()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import android.content.Intent
4 | import android.graphics.drawable.Drawable
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.core.view.doOnLayout
8 | import androidx.vectordrawable.graphics.drawable.Animatable2Compat
9 | import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
10 | import dev.chrisbanes.insetter.setEdgeToEdgeSystemUiFlags
11 | import dev.sasikanth.pinnit.activity.MainActivity
12 | import dev.sasikanth.pinnit.databinding.ActivitySplashBinding
13 |
14 | class SplashActivity : AppCompatActivity() {
15 |
16 | private val animationCallback = object : Animatable2Compat.AnimationCallback() {
17 | override fun onAnimationEnd(drawable: Drawable?) {
18 | super.onAnimationEnd(drawable)
19 | val intent = Intent(this@SplashActivity, MainActivity::class.java)
20 | startActivity(intent)
21 | finish()
22 | }
23 | }
24 |
25 | private var animatedWelcomeImage: AnimatedVectorDrawableCompat? = null
26 |
27 | private lateinit var binding: ActivitySplashBinding
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | binding = ActivitySplashBinding.inflate(layoutInflater)
32 | setContentView(binding.root)
33 |
34 | binding.rootView.setEdgeToEdgeSystemUiFlags()
35 |
36 | animatedWelcomeImage = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_pin_welcome)
37 |
38 | binding.welcomeImage.apply {
39 | setImageDrawable(animatedWelcomeImage)
40 | doOnLayout {
41 | animatedWelcomeImage?.start()
42 | animatedWelcomeImage?.registerAnimationCallback(animationCallback)
43 | }
44 | }
45 | }
46 |
47 | override fun onDestroy() {
48 | animatedWelcomeImage?.unregisterAnimationCallback(animationCallback)
49 | super.onDestroy()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/about/AboutBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.about
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.core.view.isVisible
11 | import androidx.fragment.app.FragmentManager
12 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
13 | import dev.sasikanth.pinnit.BuildConfig
14 | import dev.sasikanth.pinnit.R
15 | import dev.sasikanth.pinnit.databinding.SheetAboutBinding
16 | import dev.sasikanth.pinnit.oemwarning.OemWarningDialog
17 | import dev.sasikanth.pinnit.oemwarning.shouldShowWarningForOEM
18 |
19 | class AboutBottomSheet : BottomSheetDialogFragment() {
20 |
21 | companion object {
22 | private const val TAG = "ABOUT_BOTTOM_SHEET"
23 | private const val PINNIT_PROJECT_URL = "https://github.com/msasikanth/pinnit"
24 |
25 | fun show(fragmentManager: FragmentManager) {
26 | AboutBottomSheet().show(fragmentManager, TAG)
27 | }
28 | }
29 |
30 | private var _binding: SheetAboutBinding? = null
31 | private val binding get() = _binding!!
32 |
33 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
34 | _binding = SheetAboutBinding.inflate(layoutInflater, container, false)
35 | return _binding?.root
36 | }
37 |
38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39 | super.onViewCreated(view, savedInstanceState)
40 |
41 | setAppVersion()
42 |
43 | // binding.contactSupportButton.setOnClickListener { sendSupportEmail() }
44 | binding.sourceCodeButton.setOnClickListener { openGitHubProject() }
45 |
46 | setupShowOemWarning()
47 | }
48 |
49 | override fun onDestroyView() {
50 | _binding = null
51 | super.onDestroyView()
52 | }
53 |
54 | private fun setupShowOemWarning() {
55 | val brandName = Build.BRAND
56 | val shouldShowOemWarning = shouldShowWarningForOEM(brandName)
57 | binding.dividerView.isVisible = shouldShowOemWarning
58 | binding.oemWarningButton.isVisible = shouldShowOemWarning
59 |
60 | binding.oemWarningButton.setOnClickListener {
61 | dismiss()
62 | OemWarningDialog.show(requireActivity().supportFragmentManager)
63 | }
64 | }
65 |
66 | private fun setAppVersion() {
67 | val versionName = BuildConfig.VERSION_NAME
68 | binding.appVersionTextView.text = getString(R.string.app_version, versionName)
69 | }
70 |
71 | private fun openGitHubProject() {
72 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(PINNIT_PROJECT_URL))
73 | startActivity(intent)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.activity
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.datastore.core.DataStore
7 | import androidx.lifecycle.Lifecycle
8 | import androidx.lifecycle.lifecycleScope
9 | import androidx.lifecycle.repeatOnLifecycle
10 | import androidx.navigation.NavController
11 | import androidx.navigation.fragment.NavHostFragment
12 | import dagger.hilt.android.AndroidEntryPoint
13 | import dev.chrisbanes.insetter.setEdgeToEdgeSystemUiFlags
14 | import dev.sasikanth.pinnit.R
15 | import dev.sasikanth.pinnit.data.preferences.AppPreferences
16 | import dev.sasikanth.pinnit.databinding.ActivityMainBinding
17 | import dev.sasikanth.pinnit.oemwarning.OemWarningDialog
18 | import dev.sasikanth.pinnit.oemwarning.shouldShowWarningForOEM
19 | import dev.sasikanth.pinnit.utils.DispatcherProvider
20 | import kotlinx.coroutines.flow.first
21 | import kotlinx.coroutines.launch
22 | import kotlinx.coroutines.withContext
23 | import java.util.Locale
24 | import javax.inject.Inject
25 |
26 | @AndroidEntryPoint
27 | class MainActivity : AppCompatActivity() {
28 |
29 | @Inject
30 | lateinit var appPreferencesStore: DataStore
31 |
32 | @Inject
33 | lateinit var dispatcherProvider: DispatcherProvider
34 |
35 | private var navController: NavController? = null
36 |
37 | private lateinit var binding: ActivityMainBinding
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 |
42 | binding = ActivityMainBinding.inflate(layoutInflater)
43 | setContentView(binding.root)
44 |
45 | binding.mainRoot.setEdgeToEdgeSystemUiFlags()
46 |
47 | val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_container) as NavHostFragment
48 | navController = navHostFragment.navController
49 |
50 | lifecycleScope.launch {
51 | repeatOnLifecycle(Lifecycle.State.RESUMED) {
52 | val isOemWarningDialogShown = withContext(dispatcherProvider.io) {
53 | appPreferencesStore.data.first().oemWarningDialog
54 | }
55 | showOemWarningDialog(isOemWarningDialogShown)
56 | }
57 | }
58 | }
59 |
60 | private suspend fun showOemWarningDialog(isOemWarningDialogShown: Boolean) {
61 | val brandName = Build.BRAND.lowercase(Locale.getDefault())
62 | if (isOemWarningDialogShown.not() && shouldShowWarningForOEM(brandName)) {
63 | appPreferencesStore.updateData { currentData ->
64 | currentData.toBuilder()
65 | .setOemWarningDialog(true)
66 | .build()
67 | }
68 |
69 | OemWarningDialog.show(supportFragmentManager)
70 | }
71 | }
72 |
73 | override fun onDestroy() {
74 | navController = null
75 | super.onDestroy()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/background/receivers/AppUpdateReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.background.receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import dagger.hilt.android.AndroidEntryPoint
7 | import dev.sasikanth.pinnit.notifications.NotificationRepository
8 | import dev.sasikanth.pinnit.utils.DispatcherProvider
9 | import dev.sasikanth.pinnit.utils.notification.NotificationUtil
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.SupervisorJob
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 | import javax.inject.Inject
15 |
16 | @AndroidEntryPoint
17 | class AppUpdateReceiver : BroadcastReceiver() {
18 |
19 | @Inject
20 | lateinit var repository: NotificationRepository
21 |
22 | @Inject
23 | lateinit var notificationUtil: NotificationUtil
24 |
25 | @Inject
26 | lateinit var dispatcherProvider: DispatcherProvider
27 |
28 | private val mainScope by lazy {
29 | CoroutineScope(SupervisorJob() + dispatcherProvider.main)
30 | }
31 |
32 | override fun onReceive(context: Context?, intent: Intent?) {
33 | if (context != null && intent != null && intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
34 | val asyncResult = goAsync()
35 |
36 | mainScope.launch {
37 | runCatching {
38 | val pinnedNotifications = repository.pinnedNotifications()
39 | withContext(dispatcherProvider.default) {
40 | notificationUtil.checkNotificationsVisibility(pinnedNotifications)
41 | }
42 | }
43 | asyncResult.finish()
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/background/receivers/BootCompletedReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.background.receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import dagger.hilt.android.AndroidEntryPoint
7 | import dev.sasikanth.pinnit.notifications.NotificationRepository
8 | import dev.sasikanth.pinnit.utils.DispatcherProvider
9 | import dev.sasikanth.pinnit.utils.notification.NotificationUtil
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.SupervisorJob
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 | import javax.inject.Inject
15 |
16 | @AndroidEntryPoint
17 | class BootCompletedReceiver : BroadcastReceiver() {
18 |
19 | @Inject
20 | lateinit var repository: NotificationRepository
21 |
22 | @Inject
23 | lateinit var notificationUtil: NotificationUtil
24 |
25 | @Inject
26 | lateinit var dispatcherProvider: DispatcherProvider
27 |
28 | private val mainScope by lazy {
29 | CoroutineScope(SupervisorJob() + dispatcherProvider.main)
30 | }
31 |
32 | override fun onReceive(context: Context?, intent: Intent?) {
33 | if (context != null && intent != null && intent.action == Intent.ACTION_BOOT_COMPLETED) {
34 | val asyncResult = goAsync()
35 |
36 | mainScope.launch {
37 | runCatching {
38 | val pinnedNotifications = repository.pinnedNotifications()
39 | withContext(dispatcherProvider.default) {
40 | notificationUtil.checkNotificationsVisibility(pinnedNotifications)
41 | }
42 | }
43 | asyncResult.finish()
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/background/receivers/DeleteNotificationReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.background.receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import dev.sasikanth.pinnit.notifications.NotificationRepository
9 | import dev.sasikanth.pinnit.utils.DispatcherProvider
10 | import dev.sasikanth.pinnit.utils.notification.NotificationUtil
11 | import kotlinx.coroutines.CoroutineExceptionHandler
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.SupervisorJob
14 | import kotlinx.coroutines.launch
15 | import java.util.UUID
16 | import javax.inject.Inject
17 |
18 | @AndroidEntryPoint
19 | class DeleteNotificationReceiver : BroadcastReceiver() {
20 |
21 | companion object {
22 | private const val TAG = "DeleteNotificationReceiver"
23 |
24 | const val ACTION_DELETE = "dev.sasikanth.pinnit.system.action.DeleteNotification"
25 | const val EXTRA_NOTIFICATION_UUID = "dev.sasikanth.system.DeleteNotificationReceiver:notificationUuid"
26 | }
27 |
28 | @Inject
29 | lateinit var repository: NotificationRepository
30 |
31 | @Inject
32 | lateinit var notificationUtil: NotificationUtil
33 |
34 | @Inject
35 | lateinit var dispatcherProvider: DispatcherProvider
36 |
37 | private val job = SupervisorJob()
38 | private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
39 | Log.d(TAG, "Failed to delete the notification.", throwable)
40 | }
41 | private val mainScope by lazy {
42 | CoroutineScope(job + dispatcherProvider.main + coroutineExceptionHandler)
43 | }
44 |
45 | override fun onReceive(context: Context?, intent: Intent?) {
46 | val notificationUuid = intent?.getStringExtra(EXTRA_NOTIFICATION_UUID)
47 |
48 | if (context != null && intent != null && intent.action == ACTION_DELETE && notificationUuid != null) {
49 | val asyncResult = goAsync()
50 |
51 | mainScope.launch {
52 | val notificationId = UUID.fromString(notificationUuid)
53 | val notification = repository.notification(notificationId)
54 | repository.updatePinStatus(notification.uuid, false)
55 | repository.deleteNotification(notification)
56 |
57 | notificationUtil.dismissNotification(notification)
58 |
59 | asyncResult.finish()
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/background/receivers/UnpinNotificationReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.background.receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import dev.sasikanth.pinnit.notifications.NotificationRepository
9 | import dev.sasikanth.pinnit.utils.DispatcherProvider
10 | import dev.sasikanth.pinnit.utils.notification.NotificationUtil
11 | import kotlinx.coroutines.CoroutineExceptionHandler
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.SupervisorJob
14 | import kotlinx.coroutines.launch
15 | import java.util.UUID
16 | import javax.inject.Inject
17 |
18 | @AndroidEntryPoint
19 | class UnpinNotificationReceiver : BroadcastReceiver() {
20 |
21 | companion object {
22 | private const val TAG = "UnpinNotification"
23 |
24 | const val ACTION_UNPIN = "dev.sasikanth.pinnit.system.action.NotificationUnpin"
25 | const val EXTRA_NOTIFICATION_UUID = "dev.sasikanth.system.UnpinNotificationReceiver:notificationUuid"
26 | }
27 |
28 | @Inject
29 | lateinit var repository: NotificationRepository
30 |
31 | @Inject
32 | lateinit var notificationUtil: NotificationUtil
33 |
34 | @Inject
35 | lateinit var dispatcherProvider: DispatcherProvider
36 |
37 | private val job = SupervisorJob()
38 | private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
39 | Log.d(TAG, "Failed to unpin the notification.", throwable)
40 | }
41 | private val mainScope by lazy {
42 | CoroutineScope(job + dispatcherProvider.main + coroutineExceptionHandler)
43 | }
44 |
45 | override fun onReceive(context: Context?, intent: Intent?) {
46 | val notificationUuid = intent?.getStringExtra(EXTRA_NOTIFICATION_UUID)
47 |
48 | if (context != null && intent != null && intent.action == ACTION_UNPIN && notificationUuid != null) {
49 | val asyncResult = goAsync()
50 |
51 | mainScope.launch {
52 | val notification = repository.notification(UUID.fromString(notificationUuid))
53 | repository.updatePinStatus(notification.uuid, false)
54 |
55 | notificationUtil.dismissNotification(notification)
56 |
57 | asyncResult.finish()
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/background/services/NotificationsListener.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.background.services
2 |
3 | import android.service.notification.NotificationListenerService
4 | import android.service.notification.StatusBarNotification
5 |
6 | // TODO: Complete this for v2
7 | class NotificationsListener : NotificationListenerService() {
8 |
9 | override fun onNotificationPosted(sbn: StatusBarNotification?) {
10 | if (sbn == null || sbn.packageName == packageName) return
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import dev.sasikanth.pinnit.utils.room.InstantRoomTypeConverter
7 | import dev.sasikanth.pinnit.utils.room.LocalDateRoomTypeConverter
8 | import dev.sasikanth.pinnit.utils.room.LocalTimeRoomTypeConverter
9 | import dev.sasikanth.pinnit.utils.room.UuidRoomTypeConverter
10 |
11 | @Database(
12 | entities = [PinnitNotification::class],
13 | version = 2
14 | )
15 | @TypeConverters(
16 | UuidRoomTypeConverter::class,
17 | InstantRoomTypeConverter::class,
18 | ScheduleTypeConverter::class,
19 | LocalDateRoomTypeConverter::class,
20 | LocalTimeRoomTypeConverter::class
21 | )
22 | abstract class AppDatabase : RoomDatabase() {
23 | abstract fun notificationDao(): PinnitNotification.RoomDao
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/PinnitNotification.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data
2 |
3 | import android.os.Parcelable
4 | import androidx.annotation.Keep
5 | import androidx.room.Dao
6 | import androidx.room.Embedded
7 | import androidx.room.Entity
8 | import androidx.room.Insert
9 | import androidx.room.OnConflictStrategy
10 | import androidx.room.PrimaryKey
11 | import androidx.room.Query
12 | import dev.zacsweers.redacted.annotations.Redacted
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.parcelize.Parcelize
15 | import java.time.Instant
16 | import java.util.UUID
17 |
18 | @Keep
19 | @Entity
20 | @Parcelize
21 | data class PinnitNotification(
22 | @PrimaryKey val uuid: UUID,
23 | @Redacted val title: String,
24 | @Redacted val content: String? = null,
25 | val isPinned: Boolean = true,
26 | val createdAt: Instant,
27 | val updatedAt: Instant,
28 | val deletedAt: Instant? = null,
29 | @Embedded
30 | val schedule: Schedule? = null
31 | ) : Parcelable {
32 |
33 | val hasSchedule: Boolean
34 | get() = schedule != null
35 |
36 | fun equalsTitleAndContent(otherTitle: String?, otherContent: String?) =
37 | title == otherTitle.orEmpty() && content.orEmpty() == otherContent.orEmpty()
38 |
39 | @Dao
40 | interface RoomDao {
41 |
42 | @Insert(onConflict = OnConflictStrategy.REPLACE)
43 | suspend fun save(notifications: List)
44 |
45 | @Query("UPDATE PinnitNotification SET isPinned = :isPinned WHERE uuid = :uuid")
46 | suspend fun updatePinStatus(uuid: UUID, isPinned: Boolean)
47 |
48 | @Query(
49 | """
50 | SELECT *
51 | FROM PinnitNotification
52 | WHERE deletedAt IS NULL
53 | ORDER BY isPinned DESC, updatedAt DESC
54 | """
55 | )
56 | fun notifications(): Flow>
57 |
58 | @Query(
59 | """
60 | SELECT *
61 | FROM PinnitNotification
62 | WHERE deletedAt IS NULL AND isPinned = 1
63 | ORDER BY updatedAt DESC
64 | """
65 | )
66 | suspend fun pinnedNotifications(): List
67 |
68 | @Query("SELECT * FROM PinnitNotification WHERE uuid = :uuid LIMIT 1")
69 | suspend fun notification(uuid: UUID): PinnitNotification
70 |
71 | @Query("UPDATE PinnitNotification SET scheduleDate = null, scheduleTime = null, scheduleType = null WHERE uuid = :notificationId")
72 | suspend fun removeSchedule(notificationId: UUID)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/Schedule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data
2 |
3 | import android.os.Parcelable
4 | import androidx.room.TypeConverter
5 | import dev.sasikanth.pinnit.utils.UserClock
6 | import kotlinx.parcelize.Parcelize
7 | import java.time.Duration
8 | import java.time.LocalDate
9 | import java.time.LocalDateTime
10 | import java.time.LocalTime
11 |
12 | @Parcelize
13 | data class Schedule(
14 | val scheduleDate: LocalDate?,
15 | val scheduleTime: LocalTime?,
16 | val scheduleType: ScheduleType?
17 | ) : Parcelable {
18 |
19 | fun removeScheduleRepeat(): Schedule {
20 | return copy(scheduleType = null)
21 | }
22 |
23 | fun addScheduleRepeat(): Schedule {
24 | return copy(scheduleType = ScheduleType.Daily)
25 | }
26 |
27 | fun scheduleDateChanged(date: LocalDate): Schedule {
28 | return copy(scheduleDate = date)
29 | }
30 |
31 | fun scheduleTimeChanged(time: LocalTime?): Schedule {
32 | return copy(scheduleTime = time)
33 | }
34 |
35 | fun scheduleTypeChanged(scheduleType: ScheduleType): Schedule {
36 | return copy(scheduleType = scheduleType)
37 | }
38 |
39 | companion object {
40 |
41 | fun default(userClock: UserClock): Schedule {
42 | val userDateTime = LocalDateTime.now(userClock)
43 | .plus(Duration.ofMinutes(10))
44 |
45 | return Schedule(
46 | scheduleDate = userDateTime.toLocalDate(),
47 | scheduleTime = userDateTime.toLocalTime(),
48 | scheduleType = ScheduleType.Daily
49 | )
50 | }
51 | }
52 | }
53 |
54 | enum class ScheduleType {
55 | Daily, Weekly, Monthly
56 | }
57 |
58 | class ScheduleTypeConverter {
59 |
60 | @TypeConverter
61 | fun toScheduleType(value: String?): ScheduleType? {
62 | return if (value != null) {
63 | enumValueOf(value)
64 | } else {
65 | null
66 | }
67 | }
68 |
69 | @TypeConverter
70 | fun fromScheduleType(scheduleType: ScheduleType?): String? {
71 | return scheduleType?.toString()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/migrations/Migration_1_2.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data.migrations
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 | import dev.sasikanth.pinnit.utils.inTransaction
6 |
7 | @Suppress("ClassName")
8 | object Migration_1_2 : Migration(1, 2) {
9 |
10 | override fun migrate(db: SupportSQLiteDatabase) {
11 | with(db) {
12 | inTransaction {
13 | execSQL(""" ALTER TABLE PinnitNotification ADD COLUMN scheduleDate TEXT DEFAULT NULL """)
14 | execSQL(""" ALTER TABLE PinnitNotification ADD COLUMN scheduleTime TEXT DEFAULT NULL """)
15 | execSQL(""" ALTER TABLE PinnitNotification ADD COLUMN scheduleType TEXT DEFAULT NULL """)
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/preferences/AppPreferencesDataStore.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data.preferences
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.dataStore
6 | import androidx.datastore.migrations.SharedPreferencesMigration
7 | import androidx.datastore.migrations.SharedPreferencesView
8 | import dev.sasikanth.pinnit.BuildConfig
9 | import dev.sasikanth.pinnit.R
10 |
11 | private const val PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}_preferences"
12 | private const val DATA_STORE_FILE_NAME = "app_prefs.pb"
13 | private const val KEY_THEME = "pref_theme"
14 | private const val KEY_OEM_WARNING_DIALOG = "pref_oem_warning_dialog"
15 |
16 | val Context.appPreferencesStore: DataStore by dataStore(
17 | fileName = DATA_STORE_FILE_NAME,
18 | serializer = AppPreferencesSerializer,
19 | produceMigrations = { context ->
20 | listOf(SharedPreferencesMigration(
21 | context = context,
22 | sharedPreferencesName = PREFERENCES_NAME,
23 | keysToMigrate = setOf(KEY_THEME, KEY_OEM_WARNING_DIALOG)
24 | ) { sharedPrefs: SharedPreferencesView, currentData: AppPreferences ->
25 | var newCurrentData = currentData
26 |
27 | if (newCurrentData.theme == AppPreferences.Theme.UNSPECIFIED) {
28 | newCurrentData = currentData.toBuilder()
29 | .setTheme(getThemeForStorageValue(context, sharedPrefs))
30 | .build()
31 | }
32 |
33 | val preferencesOemWarningDialog = sharedPrefs.getBoolean(KEY_OEM_WARNING_DIALOG, false)
34 | if (newCurrentData.oemWarningDialog != preferencesOemWarningDialog) {
35 | newCurrentData = newCurrentData.toBuilder()
36 | .setOemWarningDialog(preferencesOemWarningDialog)
37 | .build()
38 | }
39 |
40 | newCurrentData
41 | })
42 | }
43 | )
44 |
45 | private fun getThemeForStorageValue(context: Context, sharedPrefs: SharedPreferencesView): AppPreferences.Theme {
46 | val defaultThemeValue = context.getString(R.string.pref_theme_auto)
47 |
48 | return when (sharedPrefs.getString(KEY_THEME, defaultThemeValue)) {
49 | context.getString(R.string.pref_theme_dark) -> AppPreferences.Theme.DARK
50 | context.getString(R.string.pref_theme_light) -> AppPreferences.Theme.LIGHT
51 | else -> AppPreferences.Theme.AUTO
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/data/preferences/AppPreferencesSerializer.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.data.preferences
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.Serializer
5 | import com.google.protobuf.InvalidProtocolBufferException
6 | import java.io.InputStream
7 | import java.io.OutputStream
8 |
9 | object AppPreferencesSerializer : Serializer {
10 |
11 | override val defaultValue: AppPreferences = AppPreferences.getDefaultInstance()
12 |
13 | override suspend fun readFrom(input: InputStream): AppPreferences {
14 | try {
15 | return AppPreferences.parseFrom(input)
16 | } catch (exception: InvalidProtocolBufferException) {
17 | throw CorruptionException("Cannot read proto.", exception)
18 | }
19 | }
20 |
21 | override suspend fun writeTo(t: AppPreferences, output: OutputStream) = t.writeTo(output)
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.spotify.mobius.android.runners.MainThreadWorkRunner
6 | import com.spotify.mobius.runners.WorkRunner
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import dev.sasikanth.pinnit.data.AppDatabase
13 | import dev.sasikanth.pinnit.data.migrations.Migration_1_2
14 | import dev.sasikanth.pinnit.utils.CoroutineDispatcherProvider
15 | import dev.sasikanth.pinnit.utils.DispatcherProvider
16 | import dev.sasikanth.pinnit.utils.RealUserClock
17 | import dev.sasikanth.pinnit.utils.UserClock
18 | import dev.sasikanth.pinnit.utils.UtcClock
19 | import java.time.ZoneId
20 | import javax.inject.Singleton
21 |
22 | @InstallIn(SingletonComponent::class)
23 | @Module
24 | object AppModule {
25 |
26 | @Singleton
27 | @Provides
28 | fun providesAppDatabase(
29 | @ApplicationContext context: Context
30 | ): AppDatabase {
31 | return Room.databaseBuilder(context, AppDatabase::class.java, "pinnit-db")
32 | .addMigrations(Migration_1_2)
33 | .build()
34 | }
35 |
36 | @Singleton
37 | @Provides
38 | fun providesUtcClock(): UtcClock = UtcClock()
39 |
40 | @Singleton
41 | @Provides
42 | fun providesUserClock(userTimeZone: ZoneId): UserClock = RealUserClock(userTimeZone)
43 |
44 | @Singleton
45 | @Provides
46 | fun providesDispatcherProvider(): DispatcherProvider = CoroutineDispatcherProvider()
47 |
48 | @Singleton
49 | @Provides
50 | fun providesSystemDefaultZone(): ZoneId = ZoneId.systemDefault()
51 |
52 | @Singleton
53 | @Provides
54 | fun providesWorkRunner(): WorkRunner = MainThreadWorkRunner.create()
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/di/DateTimeFormat.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class DateTimeFormat(val value: Type) {
7 |
8 | enum class Type {
9 | ScheduleDateFormat,
10 | ScheduleTimeFormat
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/di/DateTimeFormatterModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import android.content.Context
4 | import android.text.format.DateFormat
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import dev.sasikanth.pinnit.di.DateTimeFormat.Type.ScheduleDateFormat
11 | import dev.sasikanth.pinnit.di.DateTimeFormat.Type.ScheduleTimeFormat
12 | import java.time.format.DateTimeFormatter
13 |
14 | @InstallIn(SingletonComponent::class)
15 | @Module
16 | object DateTimeFormatterModule {
17 |
18 | @Provides
19 | @DateTimeFormat(ScheduleDateFormat)
20 | fun providesScheduleDateFormat(): DateTimeFormatter {
21 | return DateTimeFormatter.ofPattern("MMM d")
22 | }
23 |
24 | @Provides
25 | @DateTimeFormat(ScheduleTimeFormat)
26 | fun providesScheduleTimeFormat(
27 | isClock24HourFormat: Boolean
28 | ): DateTimeFormatter {
29 | return if (isClock24HourFormat) {
30 | DateTimeFormatter.ofPattern("HH:mm")
31 | } else {
32 | DateTimeFormatter.ofPattern("h:mm a")
33 | }
34 | }
35 |
36 | @Provides
37 | fun providesIsClock24HourFormat(
38 | @ApplicationContext context: Context
39 | ): Boolean {
40 | return DateFormat.is24HourFormat(context)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/di/PreferencesModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import dev.sasikanth.pinnit.data.preferences.AppPreferences
11 | import dev.sasikanth.pinnit.data.preferences.appPreferencesStore
12 | import javax.inject.Singleton
13 |
14 | @InstallIn(SingletonComponent::class)
15 | @Module
16 | object PreferencesModule {
17 |
18 | @Singleton
19 | @Provides
20 | fun providesAppPreferencesStore(
21 | @ApplicationContext context: Context
22 | ): DataStore {
23 | return context.appPreferencesStore
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/di/WorkManagerModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.di
2 |
3 | import android.content.Context
4 | import androidx.hilt.work.HiltWorkerFactory
5 | import androidx.work.Configuration
6 | import androidx.work.WorkManager
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 |
14 | @InstallIn(SingletonComponent::class)
15 | @Module
16 | object WorkManagerModule {
17 |
18 | @Singleton
19 | @Provides
20 | fun providesWorkManager(
21 | @ApplicationContext context: Context
22 | ): WorkManager = WorkManager.getInstance(context)
23 |
24 | @Singleton
25 | @Provides
26 | fun providesWorkManagerConfiguration(
27 | workerFactory: HiltWorkerFactory
28 | ): Configuration {
29 | return Configuration.Builder()
30 | .setWorkerFactory(workerFactory)
31 | .build()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/CurrentDateValidator.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import com.google.android.material.datepicker.CalendarConstraints
6 | import dev.sasikanth.pinnit.utils.RealUserClock
7 | import dev.sasikanth.pinnit.utils.UserClock
8 | import dev.sasikanth.pinnit.utils.UtcClock
9 | import java.time.Instant
10 | import java.time.LocalDate
11 | import java.time.ZoneId
12 | import javax.inject.Inject
13 |
14 | /**
15 | * Only enables date selection in [com.google.android.material.datepicker.MaterialDatePicker]
16 | * when the date is greater or equal to the current date.
17 | */
18 | class CurrentDateValidator @Inject constructor(
19 | userClock: UserClock,
20 | private val utcClock: UtcClock
21 | ) : CalendarConstraints.DateValidator {
22 |
23 | private val currentDate = LocalDate.now(userClock)
24 |
25 | override fun describeContents(): Int {
26 | return 0
27 | }
28 |
29 | override fun writeToParcel(parcel: Parcel, flags: Int) {
30 | parcel.writeInt(flags)
31 | }
32 |
33 | override fun isValid(date: Long): Boolean {
34 | val instant = Instant.ofEpochMilli(date)
35 | val localDate = instant.atZone(utcClock.zone).toLocalDate()
36 |
37 | return localDate >= currentDate
38 | }
39 |
40 | companion object CREATOR : Parcelable.Creator {
41 |
42 | override fun createFromParcel(parcel: Parcel): CurrentDateValidator {
43 | return CurrentDateValidator(
44 | userClock = RealUserClock(ZoneId.systemDefault()),
45 | utcClock = UtcClock()
46 | )
47 | }
48 |
49 | override fun newArray(size: Int): Array {
50 | return arrayOfNulls(size)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import dev.sasikanth.pinnit.data.Schedule
5 | import java.time.LocalDate
6 | import java.time.LocalTime
7 | import java.util.UUID
8 |
9 | sealed class EditorScreenEffect
10 |
11 | data class LoadNotification(val uuid: UUID) : EditorScreenEffect()
12 |
13 | data class SetTitleAndContent(val title: String?, val content: String?) : EditorScreenEffect()
14 |
15 | data class SaveNotification(
16 | val title: String,
17 | val content: String?,
18 | val schedule: Schedule?,
19 | val canPinNotification: Boolean
20 | ) : EditorScreenEffect()
21 |
22 | data class UpdateNotification(
23 | val notificationUuid: UUID,
24 | val title: String,
25 | val content: String?,
26 | val schedule: Schedule?
27 | ) : EditorScreenEffect()
28 |
29 | data object CloseEditor : EditorScreenEffect()
30 |
31 | data object ShowConfirmExitEditor : EditorScreenEffect()
32 |
33 | data class DeleteNotification(val notification: PinnitNotification) : EditorScreenEffect()
34 |
35 | data object ShowConfirmDelete : EditorScreenEffect()
36 |
37 | data class ShowDatePicker(val date: LocalDate) : EditorScreenEffect()
38 |
39 | data class ShowTimePicker(var time: LocalTime) : EditorScreenEffect()
40 |
41 | data class ShowNotification(val notification: PinnitNotification) : EditorScreenEffect()
42 |
43 | data class ScheduleNotification(val notification: PinnitNotification) : EditorScreenEffect()
44 |
45 | data class CancelNotificationSchedule(val notificationId: UUID) : EditorScreenEffect()
46 |
47 | data class ValidateSchedule(val scheduleDate: LocalDate, val scheduleTime: LocalTime) : EditorScreenEffect()
48 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import dev.sasikanth.pinnit.data.Schedule
5 | import dev.sasikanth.pinnit.data.ScheduleType
6 | import java.time.LocalDate
7 | import java.time.LocalTime
8 |
9 | sealed class EditorScreenEvent
10 |
11 | data class NotificationLoaded(val notification: PinnitNotification) : EditorScreenEvent()
12 |
13 | data class TitleChanged(val title: String) : EditorScreenEvent()
14 |
15 | data class ContentChanged(val content: String?) : EditorScreenEvent()
16 |
17 | data object SaveClicked : EditorScreenEvent()
18 |
19 | data object BackClicked : EditorScreenEvent()
20 |
21 | data object ConfirmedExit : EditorScreenEvent()
22 |
23 | data object DeleteNotificationClicked : EditorScreenEvent()
24 |
25 | data object ConfirmDeleteNotification : EditorScreenEvent()
26 |
27 | data class AddScheduleClicked(val schedule: Schedule) : EditorScreenEvent()
28 |
29 | data object RemoveScheduleClicked : EditorScreenEvent()
30 |
31 | data object ScheduleRepeatUnchecked : EditorScreenEvent()
32 |
33 | data object ScheduleRepeatChecked : EditorScreenEvent()
34 |
35 | data object ScheduleDateClicked : EditorScreenEvent()
36 |
37 | data object ScheduleTimeClicked : EditorScreenEvent()
38 |
39 | data class ScheduleDateChanged(val date: LocalDate) : EditorScreenEvent()
40 |
41 | data class ScheduleTimeChanged(val time: LocalTime) : EditorScreenEvent()
42 |
43 | data class ScheduleTypeChanged(val scheduleType: ScheduleType) : EditorScreenEvent()
44 |
45 | data class NotificationSaved(val notification: PinnitNotification) : EditorScreenEvent()
46 |
47 | data class NotificationUpdated(val updatedNotification: PinnitNotification) : EditorScreenEvent()
48 |
49 | data class ScheduleValidated(val result: ScheduleValidator.Result) : EditorScreenEvent()
50 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenInit.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import com.spotify.mobius.First
4 | import com.spotify.mobius.First.first
5 | import com.spotify.mobius.Init
6 | import javax.inject.Inject
7 |
8 | class EditorScreenInit @Inject constructor() : Init {
9 |
10 | override fun init(model: EditorScreenModel): First {
11 | if (model.notificationUuid != null) {
12 | // We are only loading notification if it's not already been loaded
13 | if (model.notification == null) {
14 | return first(model, setOf(LoadNotification(model.notificationUuid)))
15 | }
16 | } else {
17 | return first(model, setOf(SetTitleAndContent(model.title, model.content)))
18 | }
19 |
20 | return first(model)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenUi.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import dev.sasikanth.pinnit.data.ScheduleType
4 | import java.time.LocalDate
5 | import java.time.LocalTime
6 |
7 | interface EditorScreenUi {
8 | fun enableSave()
9 | fun disableSave()
10 | fun renderSaveActionButtonText()
11 | fun renderSaveAndPinActionButtonText()
12 | fun showDeleteButton()
13 | fun hideDeleteButton()
14 | fun showScheduleView()
15 | fun renderScheduleDateTime(scheduleDate: LocalDate, scheduleTime: LocalTime)
16 | fun renderScheduleRepeat(scheduleType: ScheduleType?, hasValidScheduleResult: Boolean)
17 | fun hideScheduleView()
18 | fun showScheduleWarning()
19 | fun hideScheduleWarning()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenUiRender.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | class EditorScreenUiRender(private val ui: EditorScreenUi) {
4 |
5 | fun render(model: EditorScreenModel) {
6 | if (model.isNotificationLoaded) {
7 | ui.renderSaveActionButtonText()
8 | ui.showDeleteButton()
9 | } else {
10 | renderSaveButton(model)
11 | ui.hideDeleteButton()
12 | }
13 |
14 | if (model.hasNotificationTitle && model.hasValidScheduleResult &&
15 | (model.hasTitleAndContentChanged || model.hasScheduleChanged)
16 | ) {
17 | ui.enableSave()
18 | } else {
19 | ui.disableSave()
20 | }
21 |
22 | renderScheduleView(model)
23 | }
24 |
25 | private fun renderSaveButton(model: EditorScreenModel) {
26 | if (model.hasSchedule) {
27 | ui.renderSaveActionButtonText()
28 | } else {
29 | ui.renderSaveAndPinActionButtonText()
30 | }
31 | }
32 |
33 | private fun renderScheduleView(model: EditorScreenModel) {
34 | if (model.hasSchedule) {
35 | val schedule = model.schedule!!
36 |
37 | ui.showScheduleView()
38 | ui.renderScheduleDateTime(scheduleDate = schedule.scheduleDate!!, scheduleTime = schedule.scheduleTime!!)
39 | ui.renderScheduleRepeat(scheduleType = schedule.scheduleType, hasValidScheduleResult = model.hasValidScheduleResult)
40 |
41 | if (!model.hasValidScheduleResult) {
42 | ui.showScheduleWarning()
43 | } else {
44 | ui.hideScheduleWarning()
45 | }
46 | } else {
47 | ui.hideScheduleView()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenViewEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import java.time.LocalDate
4 | import java.time.LocalTime
5 |
6 | sealed class EditorScreenViewEffect
7 |
8 | data object CloseEditorView : EditorScreenViewEffect()
9 |
10 | data class SetTitle(val title: String?) : EditorScreenViewEffect()
11 |
12 | data class SetContent(val content: String?) : EditorScreenViewEffect()
13 |
14 | data object ShowConfirmExitEditorDialog : EditorScreenViewEffect()
15 |
16 | data object ShowConfirmDeleteDialog : EditorScreenViewEffect()
17 |
18 | data class ShowDatePickerDialog(val date: LocalDate) : EditorScreenViewEffect()
19 |
20 | data class ShowTimePickerDialog(val time: LocalTime) : EditorScreenViewEffect()
21 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.spotify.mobius.Mobius
5 | import com.spotify.mobius.android.MobiusLoopViewModel
6 | import com.spotify.mobius.runners.WorkRunner
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import dev.sasikanth.pinnit.utils.MAX_VIEW_EFFECTS_QUEUE_SIZE
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class EditorScreenViewModel @Inject constructor(
13 | private val savedStateHandle: SavedStateHandle,
14 | update: EditorScreenUpdate,
15 | init: EditorScreenInit,
16 | effectHandlerFactory: EditorScreenEffectHandler.Factory,
17 | workRunner: WorkRunner
18 | ) : MobiusLoopViewModel(
19 | { viewEffectsConsumer ->
20 | Mobius.loop(update, effectHandlerFactory.create(viewEffectsConsumer).build())
21 | },
22 | editorScreenModel(savedStateHandle),
23 | init,
24 | workRunner,
25 | MAX_VIEW_EFFECTS_QUEUE_SIZE
26 | ) {
27 |
28 | companion object {
29 | private const val MODEL_KEY = "EDITOR_SCREEN_MODEL"
30 |
31 | private fun editorScreenModel(savedStateHandle: SavedStateHandle): EditorScreenModel {
32 | val args = EditorScreenArgs.fromSavedStateHandle(savedStateHandle)
33 |
34 | return savedStateHandle.get(MODEL_KEY) ?: EditorScreenModel.default(
35 | notificationUuid = args.notificationUuid,
36 | title = args.notificationTitle,
37 | content = args.notificationContent
38 | )
39 | }
40 | }
41 |
42 | override fun onClearedInternal() {
43 | savedStateHandle[MODEL_KEY] = model
44 | super.onClearedInternal()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/EditorTransition.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import android.os.Parcelable
4 | import androidx.annotation.Keep
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Keep
8 | sealed class EditorTransition : Parcelable {
9 |
10 | @Parcelize
11 | data object SharedAxis : EditorTransition()
12 |
13 | @Parcelize
14 | data class ContainerTransform(val transitionName: String) : EditorTransition()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/editor/ScheduleValidator.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import android.os.Parcelable
4 | import dev.sasikanth.pinnit.editor.ScheduleValidator.Result.ScheduleInPastError
5 | import dev.sasikanth.pinnit.editor.ScheduleValidator.Result.Valid
6 | import dev.sasikanth.pinnit.utils.UserClock
7 | import kotlinx.parcelize.Parcelize
8 | import java.time.LocalDate
9 | import java.time.LocalDateTime
10 | import java.time.LocalTime
11 | import javax.inject.Inject
12 |
13 | class ScheduleValidator @Inject constructor(val userClock: UserClock) {
14 |
15 | fun validate(scheduleDate: LocalDate, scheduleTime: LocalTime): Result {
16 | val scheduleDateTime = scheduleDate.atTime(scheduleTime)
17 | val now = LocalDateTime.now(userClock)
18 |
19 | if (scheduleDateTime.isBefore(now)) {
20 | return ScheduleInPastError
21 | }
22 |
23 | return Valid
24 | }
25 |
26 | sealed class Result : Parcelable {
27 |
28 | @Parcelize
29 | data object Valid : Result()
30 |
31 | @Parcelize
32 | data object ScheduleInPastError : Result()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dev.sasikanth.pinnit.data.AppDatabase
8 | import dev.sasikanth.pinnit.data.PinnitNotification
9 | import javax.inject.Singleton
10 |
11 | @InstallIn(SingletonComponent::class)
12 | @Module
13 | object NotificationModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun providesNotificationDao(appDatabase: AppDatabase): PinnitNotification.RoomDao {
18 | return appDatabase.notificationDao()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import dev.sasikanth.pinnit.data.Schedule
5 | import dev.sasikanth.pinnit.utils.UtcClock
6 | import kotlinx.coroutines.flow.Flow
7 | import java.time.Instant
8 | import java.util.UUID
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class NotificationRepository @Inject constructor(
14 | private val notificationDao: PinnitNotification.RoomDao,
15 | private val utcClock: UtcClock
16 | ) {
17 |
18 | suspend fun save(
19 | title: String,
20 | content: String? = null,
21 | isPinned: Boolean = true,
22 | schedule: Schedule? = null,
23 | uuid: UUID = UUID.randomUUID()
24 | ): PinnitNotification {
25 | val notification = PinnitNotification(
26 | uuid = uuid,
27 | title = title,
28 | content = content,
29 | isPinned = isPinned,
30 | schedule = schedule,
31 | createdAt = Instant.now(utcClock),
32 | updatedAt = Instant.now(utcClock),
33 | deletedAt = null
34 | )
35 | notificationDao.save(listOf(notification))
36 | return notification
37 | }
38 |
39 | suspend fun save(notifications: List) {
40 | notificationDao.save(notifications)
41 | }
42 |
43 | suspend fun updateNotification(notification: PinnitNotification): PinnitNotification {
44 | val updatedNotification = notification.copy(
45 | updatedAt = Instant.now(utcClock)
46 | )
47 | notificationDao.save(listOf(updatedNotification))
48 | return updatedNotification
49 | }
50 |
51 | suspend fun notification(uuid: UUID): PinnitNotification {
52 | return notificationDao.notification(uuid)
53 | }
54 |
55 | suspend fun updatePinStatus(notificationUuid: UUID, isPinned: Boolean) {
56 | notificationDao.updatePinStatus(notificationUuid, isPinned)
57 | }
58 |
59 | fun notifications(): Flow> {
60 | return notificationDao.notifications()
61 | }
62 |
63 | suspend fun pinnedNotifications(): List {
64 | return notificationDao.pinnedNotifications()
65 | }
66 |
67 | suspend fun deleteNotification(notification: PinnitNotification): PinnitNotification {
68 | val deletedNotification = notification.copy(
69 | deletedAt = Instant.now(utcClock)
70 | )
71 | notificationDao.save(listOf(deletedNotification))
72 | return deletedNotification
73 | }
74 |
75 | suspend fun undoNotificationDelete(notification: PinnitNotification) {
76 | val undidNotification = notification.copy(
77 | deletedAt = null
78 | )
79 | notificationDao.save(listOf(undidNotification))
80 | }
81 |
82 | suspend fun removeSchedule(notificationId: UUID) {
83 | notificationDao.removeSchedule(notificationId)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationScreenViewEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import java.util.UUID
4 |
5 | sealed class NotificationScreenViewEffect
6 |
7 | data class UndoNotificationDeleteViewEffect(val notificationUuid: UUID) : NotificationScreenViewEffect()
8 |
9 | data object RequestNotificationPermissionViewEffect : NotificationScreenViewEffect()
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import java.util.UUID
5 |
6 | sealed class NotificationsScreenEffect
7 |
8 | data object LoadNotifications : NotificationsScreenEffect()
9 |
10 | data class ToggleNotificationPinStatus(val notification: PinnitNotification) : NotificationsScreenEffect()
11 |
12 | data class DeleteNotification(val notification: PinnitNotification) : NotificationsScreenEffect()
13 |
14 | data class UndoDeletedNotification(val notificationUuid: UUID) : NotificationsScreenEffect()
15 |
16 | data object CheckNotificationsVisibility : NotificationsScreenEffect()
17 |
18 | data class ShowUndoDeleteNotification(val notification: PinnitNotification) : NotificationsScreenEffect()
19 |
20 | data class CancelNotificationSchedule(val notificationId: UUID) : NotificationsScreenEffect()
21 |
22 | data class RemoveSchedule(val notificationId: UUID) : NotificationsScreenEffect()
23 |
24 | data class ScheduleNotification(val notification: PinnitNotification) : NotificationsScreenEffect()
25 |
26 | data object CheckPermissionToPostNotification : NotificationsScreenEffect()
27 |
28 | data object RequestNotificationPermission : NotificationsScreenEffect()
29 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import java.util.UUID
5 |
6 | sealed class NotificationsScreenEvent
7 |
8 | data class NotificationsLoaded(val notifications: List) : NotificationsScreenEvent()
9 |
10 | data class NotificationSwiped(val notification: PinnitNotification) : NotificationsScreenEvent()
11 |
12 | data class TogglePinStatusClicked(val notification: PinnitNotification) : NotificationsScreenEvent()
13 |
14 | data class UndoNotificationDelete(val notificationUuid: UUID) : NotificationsScreenEvent()
15 |
16 | data class NotificationDeleted(val notification: PinnitNotification) : NotificationsScreenEvent()
17 |
18 | data class RemovedNotificationSchedule(val notificationId: UUID) : NotificationsScreenEvent()
19 |
20 | data class RemoveNotificationScheduleClicked(val notificationId: UUID) : NotificationsScreenEvent()
21 |
22 | data class RestoredDeletedNotification(val notification: PinnitNotification) : NotificationsScreenEvent()
23 |
24 | data class HasPermissionToPostNotifications(val canPostNotifications: Boolean) : NotificationsScreenEvent()
25 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import com.spotify.mobius.First
4 | import com.spotify.mobius.First.first
5 | import com.spotify.mobius.Init
6 | import javax.inject.Inject
7 |
8 | class NotificationsScreenInit @Inject constructor() : Init {
9 | override fun init(model: NotificationsScreenModel): First {
10 | val effects = mutableSetOf(CheckPermissionToPostNotification)
11 |
12 | if (model.notificationsQueried.not()) {
13 | // We are only checking for notifications visibility during
14 | // screen create because system notifications are disappear only
15 | // if the app is force closed (or when updating). So app needs
16 | // to be reopened again. Notifications will persist
17 | // orientation changes, so no point checking again.
18 | effects.addAll(listOf(LoadNotifications, CheckNotificationsVisibility))
19 | }
20 |
21 | return first(model, effects)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import android.os.Parcelable
4 | import dev.sasikanth.pinnit.data.PinnitNotification
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | data class NotificationsScreenModel(
9 | val notifications: List?
10 | ) : Parcelable {
11 |
12 | companion object {
13 | fun default() = NotificationsScreenModel(notifications = null)
14 | }
15 |
16 | val notificationsQueried: Boolean
17 | get() = !notifications.isNullOrEmpty()
18 |
19 | fun onNotificationsLoaded(notifications: List) = copy(notifications = notifications)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUiRender.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 |
5 | interface NotificationsScreenUi {
6 | fun showNotifications(notifications: List)
7 | fun showNotificationsEmptyError()
8 | fun hideNotificationsEmptyError()
9 | fun hideNotifications()
10 | }
11 |
12 | class NotificationsScreenUiRender(
13 | private val ui: NotificationsScreenUi
14 | ) {
15 |
16 | fun render(model: NotificationsScreenModel) {
17 | if (model.notifications == null) {
18 | return
19 | }
20 |
21 | if (model.notifications.isNotEmpty()) {
22 | ui.hideNotificationsEmptyError()
23 | ui.showNotifications(model.notifications)
24 | } else {
25 | ui.hideNotifications()
26 | ui.showNotificationsEmptyError()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import com.spotify.mobius.Next
4 | import com.spotify.mobius.Next.dispatch
5 | import com.spotify.mobius.Next.next
6 | import com.spotify.mobius.Next.noChange
7 | import com.spotify.mobius.Update
8 | import javax.inject.Inject
9 |
10 | class NotificationsScreenUpdate @Inject constructor() : Update {
11 | override fun update(model: NotificationsScreenModel, event: NotificationsScreenEvent): Next {
12 | return when (event) {
13 | is NotificationsLoaded -> next(model.onNotificationsLoaded(event.notifications))
14 | is NotificationSwiped -> dispatch(setOf(DeleteNotification(event.notification)))
15 | is TogglePinStatusClicked -> dispatch(setOf(ToggleNotificationPinStatus(event.notification)))
16 | is UndoNotificationDelete -> dispatch(setOf(UndoDeletedNotification(event.notificationUuid)))
17 | is NotificationDeleted -> notificationDeleted(event)
18 | is RemovedNotificationSchedule -> dispatch(setOf(CancelNotificationSchedule(event.notificationId)))
19 | is RemoveNotificationScheduleClicked -> dispatch(setOf(RemoveSchedule(event.notificationId)))
20 | is RestoredDeletedNotification -> dispatch(setOf(ScheduleNotification(event.notification)))
21 | is HasPermissionToPostNotifications -> hasPermissionToPostNotifications(event.canPostNotifications)
22 | }
23 | }
24 |
25 | private fun hasPermissionToPostNotifications(canPostNotifications: Boolean): Next {
26 | return if (!canPostNotifications) {
27 | dispatch(setOf(RequestNotificationPermission))
28 | } else {
29 | noChange()
30 | }
31 | }
32 |
33 | private fun notificationDeleted(event: NotificationDeleted): Next {
34 | val notification = event.notification
35 | val effects = mutableSetOf(ShowUndoDeleteNotification(event.notification))
36 |
37 | if (notification.hasSchedule) {
38 | effects.add(CancelNotificationSchedule(notification.uuid))
39 | }
40 |
41 | return dispatch(effects)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.spotify.mobius.Mobius
5 | import com.spotify.mobius.android.MobiusLoopViewModel
6 | import com.spotify.mobius.runners.WorkRunner
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import dev.sasikanth.pinnit.utils.MAX_VIEW_EFFECTS_QUEUE_SIZE
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class NotificationsScreenViewModel @Inject constructor(
13 | private val savedStateHandle: SavedStateHandle,
14 | update: NotificationsScreenUpdate,
15 | init: NotificationsScreenInit,
16 | effectHandlerFactory: NotificationsScreenEffectHandler.Factory,
17 | workRunner: WorkRunner
18 | ) : MobiusLoopViewModel(
19 | { viewEffectsConsumer ->
20 | Mobius.loop(update, effectHandlerFactory.create(viewEffectsConsumer).build())
21 | },
22 | savedStateHandle.get(MODEL_KEY) ?: NotificationsScreenModel.default(),
23 | init,
24 | workRunner,
25 | MAX_VIEW_EFFECTS_QUEUE_SIZE
26 | ) {
27 |
28 | companion object {
29 | private const val MODEL_KEY = "NOTIFICATIONS_SCREEN_MODEL"
30 | }
31 |
32 | override fun onClearedInternal() {
33 | savedStateHandle[MODEL_KEY] = model
34 | super.onClearedInternal()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/adapter/NotificationPinItemAnimator.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications.adapter
2 |
3 | import android.view.animation.AccelerateInterpolator
4 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator
5 | import androidx.recyclerview.widget.DefaultItemAnimator
6 | import androidx.recyclerview.widget.RecyclerView
7 | import dev.sasikanth.pinnit.notifications.adapter.NotificationsListAdapter.NotificationViewHolder
8 | import java.util.UUID
9 |
10 | class NotificationPinItemAnimator : DefaultItemAnimator() {
11 |
12 | private val accelerateInterpolator = AccelerateInterpolator()
13 | private val fastOutSlowInInterpolator = FastOutSlowInInterpolator()
14 |
15 | override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
16 | return true
17 | }
18 |
19 | private fun getItemHolderInfo(
20 | viewHolder: RecyclerView.ViewHolder,
21 | info: NotificationItemInfo
22 | ): ItemHolderInfo {
23 | val notification = (viewHolder as NotificationViewHolder).notification
24 | if (notification != null) {
25 | info.uuid = notification.uuid
26 | info.isPinned = notification.isPinned
27 | }
28 | return info
29 | }
30 |
31 | override fun obtainHolderInfo(): ItemHolderInfo {
32 | return NotificationItemInfo()
33 | }
34 |
35 | override fun recordPreLayoutInformation(
36 | state: RecyclerView.State,
37 | viewHolder: RecyclerView.ViewHolder,
38 | changeFlags: Int,
39 | payloads: MutableList
40 | ): ItemHolderInfo {
41 | val notificationItemInfo = super.recordPreLayoutInformation(
42 | state,
43 | viewHolder,
44 | changeFlags,
45 | payloads
46 | ) as NotificationItemInfo
47 | return getItemHolderInfo(viewHolder, notificationItemInfo)
48 | }
49 |
50 | override fun recordPostLayoutInformation(
51 | state: RecyclerView.State,
52 | viewHolder: RecyclerView.ViewHolder
53 | ): ItemHolderInfo {
54 | val notificationItemInfo = super.recordPostLayoutInformation(state, viewHolder) as NotificationItemInfo
55 | return getItemHolderInfo(viewHolder, notificationItemInfo)
56 | }
57 |
58 | override fun animateChange(
59 | oldHolder: RecyclerView.ViewHolder,
60 | newHolder: RecyclerView.ViewHolder,
61 | preInfo: ItemHolderInfo,
62 | postInfo: ItemHolderInfo
63 | ): Boolean {
64 | if (newHolder != oldHolder) {
65 | return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
66 | }
67 |
68 | if ((newHolder as? NotificationViewHolder != null) &&
69 | (oldHolder as? NotificationViewHolder != null) &&
70 | (preInfo as? NotificationItemInfo != null) &&
71 | (postInfo as? NotificationItemInfo != null)
72 | ) {
73 | val oldPinStatus = preInfo.isPinned
74 | val newPinStatus = postInfo.isPinned
75 |
76 | if (oldPinStatus == newPinStatus || preInfo.uuid != postInfo.uuid) {
77 | return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
78 | }
79 |
80 | return try {
81 | newHolder.animateReveal(
82 | newPinStatus = newPinStatus,
83 | fastOutSlowInInterpolator = fastOutSlowInInterpolator,
84 | accelerateInterpolator = accelerateInterpolator,
85 | dispatchAnimationFinished = {
86 | dispatchAnimationFinished(newHolder)
87 | }
88 | )
89 | true
90 | } catch (e: Exception) {
91 | super.animateChange(oldHolder, newHolder, preInfo, postInfo)
92 | }
93 | } else {
94 | return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
95 | }
96 | }
97 |
98 | data class NotificationItemInfo(
99 | var uuid: UUID? = null,
100 | var isPinned: Boolean = false
101 | ) : ItemHolderInfo()
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/adapter/NotificationsDiffCallback.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications.adapter
2 |
3 | import androidx.recyclerview.widget.DiffUtil
4 | import dev.sasikanth.pinnit.data.PinnitNotification
5 |
6 | object NotificationsDiffCallback : DiffUtil.ItemCallback() {
7 | override fun areItemsTheSame(oldItem: PinnitNotification, newItem: PinnitNotification): Boolean {
8 | return oldItem.uuid == newItem.uuid
9 | }
10 |
11 | override fun areContentsTheSame(oldItem: PinnitNotification, newItem: PinnitNotification): Boolean {
12 | return oldItem == newItem
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/notifications/adapter/NotificationsItemTouchHelper.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications.adapter
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.drawable.ColorDrawable
6 | import androidx.core.content.ContextCompat
7 | import androidx.recyclerview.widget.ItemTouchHelper
8 | import androidx.recyclerview.widget.ItemTouchHelper.LEFT
9 | import androidx.recyclerview.widget.ItemTouchHelper.RIGHT
10 | import androidx.recyclerview.widget.RecyclerView
11 | import dev.sasikanth.pinnit.R
12 | import dev.sasikanth.pinnit.data.PinnitNotification
13 | import dev.sasikanth.pinnit.notifications.adapter.NotificationsListAdapter.NotificationViewHolder
14 | import dev.sasikanth.pinnit.utils.px
15 | import dev.sasikanth.pinnit.utils.resolveColor
16 |
17 | class NotificationsItemTouchHelper(
18 | context: Context,
19 | private val adapter: NotificationsListAdapter,
20 | private val onItemSwiped: (notification: PinnitNotification) -> Unit
21 | ) : ItemTouchHelper.SimpleCallback(0, LEFT or RIGHT) {
22 |
23 | private val colorDrawable = ColorDrawable(
24 | context.resolveColor(attrRes = R.attr.colorRowBackground)
25 | )
26 | private val icon = ContextCompat.getDrawable(context, R.drawable.ic_pinnit_delete)
27 |
28 | override fun isItemViewSwipeEnabled(): Boolean {
29 | return true
30 | }
31 |
32 | override fun isLongPressDragEnabled(): Boolean {
33 | return false
34 | }
35 |
36 | override fun getSwipeDirs(
37 | recyclerView: RecyclerView,
38 | viewHolder: RecyclerView.ViewHolder
39 | ): Int {
40 | if (viewHolder is NotificationViewHolder) {
41 | val notification = adapter.currentList[viewHolder.adapterPosition]
42 | if (notification.isPinned) {
43 | return 0
44 | }
45 | }
46 | return super.getSwipeDirs(recyclerView, viewHolder)
47 | }
48 |
49 | override fun onMove(
50 | recyclerView: RecyclerView,
51 | viewHolder: RecyclerView.ViewHolder,
52 | target: RecyclerView.ViewHolder
53 | ): Boolean {
54 | return false
55 | }
56 |
57 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
58 | val adapterPosition = viewHolder.adapterPosition
59 | if (adapterPosition != RecyclerView.NO_POSITION) {
60 | if (viewHolder is NotificationViewHolder) {
61 | val notification = adapter.currentList[adapterPosition]
62 | onItemSwiped(notification)
63 | }
64 | }
65 | }
66 |
67 | override fun onChildDraw(
68 | c: Canvas,
69 | recyclerView: RecyclerView,
70 | viewHolder: RecyclerView.ViewHolder,
71 | dX: Float,
72 | dY: Float,
73 | actionState: Int,
74 | isCurrentlyActive: Boolean
75 | ) {
76 | val itemView = viewHolder.itemView
77 | val iconMarginH = 24.px
78 | val cellHeight = itemView.bottom - itemView.top
79 | val iconWidth = icon!!.intrinsicWidth
80 | val iconHeight = icon.intrinsicHeight
81 | val iconTop = itemView.top + (cellHeight - iconHeight) / 2
82 | val iconBottom = iconTop + iconHeight
83 |
84 | val iconLeft: Int
85 | val iconRight: Int
86 |
87 | if (dX > 0) {
88 | // Right Swipe
89 | colorDrawable.setBounds(itemView.left, itemView.top, dX.toInt(), itemView.bottom)
90 | iconLeft = iconMarginH
91 | iconRight = iconMarginH + iconWidth
92 | } else {
93 | // Left Swipe
94 | colorDrawable.setBounds(
95 | itemView.right + dX.toInt(),
96 | itemView.top,
97 | itemView.right,
98 | itemView.bottom
99 | )
100 | iconLeft = itemView.right - iconMarginH - iconWidth
101 | iconRight = itemView.right - iconMarginH
102 | }
103 |
104 | icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
105 |
106 | colorDrawable.draw(c)
107 | icon.draw(c)
108 |
109 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/oemwarning/OemChecker.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.oemwarning
2 |
3 | private const val BRAND_XIAOMI = "xiaomi"
4 | private const val BRAND_REDMI = "redmi"
5 | private const val BRAND_HONOR = "honor"
6 | private const val BRAND_HUAWEI = "huawei"
7 | private const val BRAND_OPPO = "oppo"
8 | private const val BRAND_VIVO = "vivo"
9 | private const val BRAND_ONEPLUS = "oneplus"
10 | private const val BRAND_MEIZU = "meizu"
11 |
12 | private val warningOems = arrayOf(
13 | BRAND_XIAOMI,
14 | BRAND_REDMI,
15 | BRAND_HONOR,
16 | BRAND_HUAWEI,
17 | BRAND_OPPO,
18 | BRAND_VIVO,
19 | BRAND_ONEPLUS,
20 | BRAND_MEIZU
21 | )
22 |
23 | fun shouldShowWarningForOEM(oemName: String): Boolean {
24 | return warningOems.contains(oemName)
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/oemwarning/OemWarningDialog.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.oemwarning
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Dialog
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import androidx.fragment.app.DialogFragment
13 | import androidx.fragment.app.FragmentManager
14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
15 | import dev.sasikanth.pinnit.databinding.PinnitOemWarningDialogBinding
16 |
17 | class OemWarningDialog : DialogFragment() {
18 |
19 | companion object {
20 | private const val TAG = "OEM_WARNING_DIALOG"
21 | private val DONT_KILL_MY_APP_LINK = "https://dontkillmyapp.com/${Build.BRAND}"
22 |
23 | fun show(fragmentManager: FragmentManager) {
24 | OemWarningDialog()
25 | .show(fragmentManager, TAG)
26 | }
27 | }
28 |
29 | private var _binding: PinnitOemWarningDialogBinding? = null
30 | private val binding get() = _binding!!
31 |
32 | @SuppressLint("InflateParams")
33 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
34 | _binding = PinnitOemWarningDialogBinding.inflate(layoutInflater, null, false)
35 |
36 | return MaterialAlertDialogBuilder(requireContext())
37 | .setView(binding.root)
38 | .create()
39 | }
40 |
41 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
42 | return binding.root
43 | }
44 |
45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
46 | super.onViewCreated(view, savedInstanceState)
47 | binding.showInstructionsButton.setOnClickListener {
48 | showInstructions()
49 | dismiss()
50 | }
51 | }
52 |
53 | override fun onDestroyView() {
54 | _binding = null
55 | super.onDestroyView()
56 | }
57 |
58 | private fun showInstructions() {
59 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(DONT_KILL_MY_APP_LINK))
60 | startActivity(intent)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/options/OptionsBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.options
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.IdRes
8 | import androidx.datastore.core.DataStore
9 | import androidx.fragment.app.FragmentManager
10 | import androidx.lifecycle.Lifecycle
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.lifecycle.repeatOnLifecycle
13 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
14 | import dagger.hilt.android.AndroidEntryPoint
15 | import dev.sasikanth.pinnit.R
16 | import dev.sasikanth.pinnit.data.preferences.AppPreferences
17 | import dev.sasikanth.pinnit.databinding.ThemeSelectionSheetBinding
18 | import dev.sasikanth.pinnit.utils.DispatcherProvider
19 | import kotlinx.coroutines.flow.filter
20 | import kotlinx.coroutines.flow.first
21 | import kotlinx.coroutines.flow.map
22 | import kotlinx.coroutines.launch
23 | import kotlinx.coroutines.withContext
24 | import reactivecircus.flowbinding.material.buttonCheckedChanges
25 | import javax.inject.Inject
26 |
27 | @AndroidEntryPoint
28 | class OptionsBottomSheet : BottomSheetDialogFragment() {
29 |
30 | @Inject
31 | lateinit var appPreferencesStore: DataStore
32 |
33 | @Inject
34 | lateinit var dispatcherProvider: DispatcherProvider
35 |
36 | companion object {
37 | private const val TAG = "OptionsBottomSheet"
38 |
39 | fun show(fragmentManager: FragmentManager) {
40 | OptionsBottomSheet()
41 | .show(fragmentManager, TAG)
42 | }
43 | }
44 |
45 | private var _binding: ThemeSelectionSheetBinding? = null
46 | private val binding get() = _binding!!
47 |
48 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
49 | _binding = ThemeSelectionSheetBinding.inflate(layoutInflater, container, false)
50 | return _binding?.root
51 | }
52 |
53 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
54 | super.onViewCreated(view, savedInstanceState)
55 |
56 | viewLifecycleOwner.lifecycleScope.launch {
57 | repeatOnLifecycle(Lifecycle.State.RESUMED) {
58 | val theme = withContext(dispatcherProvider.io) {
59 | appPreferencesStore.data.first().theme
60 | }
61 | checkThemeSelection(theme)
62 |
63 | binding.themeButtonGroup
64 | .buttonCheckedChanges()
65 | .filter { it.checked }
66 | .map { it.checkedId }
67 | .collect { updateTheme(it) }
68 | }
69 | }
70 | }
71 |
72 | override fun onDestroyView() {
73 | _binding = null
74 | super.onDestroyView()
75 | }
76 |
77 | private fun checkThemeSelection(theme: AppPreferences.Theme) {
78 | val id = when (theme) {
79 | AppPreferences.Theme.AUTO -> R.id.darkModeAuto
80 | AppPreferences.Theme.LIGHT -> R.id.darkModeOff
81 | AppPreferences.Theme.DARK -> R.id.darkModeOn
82 | else -> R.id.darkModeAuto
83 | }
84 |
85 | binding.themeButtonGroup.check(id)
86 | }
87 |
88 | private suspend fun updateTheme(@IdRes checkedId: Int) {
89 | val theme = when (checkedId) {
90 | R.id.darkModeOn -> AppPreferences.Theme.DARK
91 | R.id.darkModeOff -> AppPreferences.Theme.LIGHT
92 | R.id.darkModeAuto -> AppPreferences.Theme.AUTO
93 | else -> throw IllegalArgumentException("Unknown theme selection")
94 | }
95 |
96 | appPreferencesStore.updateData { currentData ->
97 | currentData.toBuilder()
98 | .setTheme(theme)
99 | .build()
100 | }
101 |
102 | dismiss()
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/scheduler/PinnitNotificationScheduler.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.scheduler
2 |
3 | import androidx.work.ExistingWorkPolicy
4 | import androidx.work.WorkManager
5 | import dev.sasikanth.pinnit.data.PinnitNotification
6 | import dev.sasikanth.pinnit.utils.UserClock
7 | import dev.sasikanth.pinnit.worker.ScheduleWorker
8 | import java.time.LocalDateTime
9 | import java.util.UUID
10 | import javax.inject.Inject
11 |
12 | class PinnitNotificationScheduler @Inject constructor(
13 | private val workManager: WorkManager,
14 | private val userClock: UserClock
15 | ) {
16 |
17 | fun scheduleNotification(notification: PinnitNotification) {
18 | if (notification.schedule == null) return
19 |
20 | val now = LocalDateTime.now(userClock)
21 | val scheduleDateTime = notification.schedule.scheduleDate!!.atTime(notification.schedule.scheduleTime)
22 |
23 | val isInFuture = scheduleDateTime.isAfter(now)
24 |
25 | if (isInFuture.not()) return
26 |
27 | val workRequest = ScheduleWorker.scheduleNotificationRequest(
28 | notificationUuid = notification.uuid,
29 | schedule = notification.schedule,
30 | userClock = userClock
31 | )
32 |
33 | workManager
34 | .enqueueUniqueWork(
35 | ScheduleWorker.tag(notificationUuid = notification.uuid),
36 | ExistingWorkPolicy.REPLACE,
37 | workRequest
38 | )
39 | }
40 |
41 | fun cancel(notificationUuid: UUID) {
42 | workManager.cancelAllWorkByTag(ScheduleWorker.tag(notificationUuid = notificationUuid))
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import android.content.Context
4 | import androidx.annotation.AttrRes
5 | import androidx.annotation.ColorInt
6 | import androidx.annotation.ColorRes
7 | import androidx.core.content.ContextCompat
8 |
9 | // Source: https://github.com/afollestad/material-dialogs/blob/66582d9993bcf55bfea873b4bf484a429ce3df36/core/src/main/java/com/afollestad/materialdialogs/utils/MDUtil.kt#L103
10 | @ColorInt
11 | fun Context.resolveColor(
12 | @ColorRes colorRes: Int? = null,
13 | @AttrRes attrRes: Int? = null,
14 | fallback: (() -> Int)? = null
15 | ): Int {
16 | if (attrRes != null) {
17 | val a = theme.obtainStyledAttributes(intArrayOf(attrRes))
18 | try {
19 | val result = a.getColor(0, 0)
20 | if (result == 0 && fallback != null) {
21 | return fallback()
22 | }
23 | return result
24 | } finally {
25 | a.recycle()
26 | }
27 | }
28 | return ContextCompat.getColor(this, colorRes ?: 0)
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/DatabaseExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import androidx.sqlite.db.SupportSQLiteDatabase
4 |
5 | fun SupportSQLiteDatabase.inTransaction(block: SupportSQLiteDatabase.() -> Unit) {
6 | beginTransaction()
7 | try {
8 | block.invoke(this)
9 | setTransactionSuccessful()
10 | } finally {
11 | endTransaction()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | interface DispatcherProvider {
7 | val main: CoroutineDispatcher
8 | val io: CoroutineDispatcher
9 | val default: CoroutineDispatcher
10 | }
11 |
12 | class CoroutineDispatcherProvider : DispatcherProvider {
13 |
14 | override val main: CoroutineDispatcher
15 | get() = Dispatchers.Main
16 |
17 | override val io: CoroutineDispatcher
18 | get() = Dispatchers.IO
19 |
20 | override val default: CoroutineDispatcher
21 | get() = Dispatchers.Default
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/DrawableExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import android.os.CountDownTimer
4 | import androidx.vectordrawable.graphics.drawable.SeekableAnimatedVectorDrawable
5 |
6 | fun SeekableAnimatedVectorDrawable.reverse() {
7 | object : CountDownTimer(totalDuration, 1) {
8 | override fun onTick(interval: Long) {
9 | currentPlayTime = interval
10 | }
11 |
12 | override fun onFinish() {
13 | currentPlayTime = 0
14 | }
15 | }.start()
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/Globals.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | const val MAX_VIEW_EFFECTS_QUEUE_SIZE = 100
4 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/UserClock.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import java.time.Clock
4 | import java.time.Instant
5 | import java.time.ZoneId
6 |
7 | // Source: https://github.com/simpledotorg/simple-android/blob/45d4b34019b31328b7593590ee5f1a2095638206/app/src/main/java/org/simple/clinic/util/Clocks.kt
8 | abstract class UserClock : Clock()
9 |
10 | class RealUserClock(private val userTimeZone: ZoneId) : UserClock() {
11 |
12 | private val userClock = system(userTimeZone)
13 |
14 | override fun getZone(): ZoneId = userTimeZone
15 |
16 | override fun withZone(zoneId: ZoneId?): Clock = userClock.withZone(zoneId)
17 |
18 | override fun instant(): Instant = userClock.instant()
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/UtcClock.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import java.time.Clock
4 | import java.time.Instant
5 | import java.time.ZoneId
6 |
7 | // Source: https://github.com/simpledotorg/simple-android/blob/45d4b34019b31328b7593590ee5f1a2095638206/app/src/main/java/org/simple/clinic/util/Clocks.kt
8 | open class UtcClock : Clock() {
9 |
10 | private val utcClock = systemUTC()
11 |
12 | override fun withZone(zoneId: ZoneId?): Clock = utcClock.withZone(zoneId)
13 |
14 | override fun getZone(): ZoneId = utcClock.zone
15 |
16 | override fun instant(): Instant = utcClock.instant()
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/ViewExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import android.content.res.Resources
4 |
5 | val Int.px: Int
6 | get() = (Resources.getSystem().displayMetrics.density * this).toInt()
7 |
8 | val Int.dp: Int
9 | get() = (this / Resources.getSystem().displayMetrics.density).toInt()
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/room/JavaTimeRoomTypeConverters.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils.room
2 |
3 | import androidx.room.TypeConverter
4 | import java.time.Instant
5 | import java.time.LocalDate
6 | import java.time.LocalTime
7 |
8 | class InstantRoomTypeConverter {
9 |
10 | @TypeConverter
11 | fun toInstant(value: String?): Instant? {
12 | return value?.let { Instant.parse(value) }
13 | }
14 |
15 | @TypeConverter
16 | fun fromInstant(instant: Instant?): String? {
17 | return instant?.toString()
18 | }
19 | }
20 |
21 | class LocalDateRoomTypeConverter {
22 |
23 | @TypeConverter
24 | fun toLocalDate(value: String?): LocalDate? {
25 | return value?.let { LocalDate.parse(value) }
26 | }
27 |
28 | @TypeConverter
29 | fun fromLocalDate(localDate: LocalDate?): String? {
30 | return localDate?.toString()
31 | }
32 | }
33 |
34 | class LocalTimeRoomTypeConverter {
35 |
36 | @TypeConverter
37 | fun toLocalTime(value: String?): LocalTime? {
38 | return value?.let { LocalTime.parse(value) }
39 | }
40 |
41 | @TypeConverter
42 | fun fromLocalTime(localTime: LocalTime?): String? {
43 | return localTime?.toString()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/utils/room/UuidRoomTypeConverter.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils.room
2 |
3 | import androidx.room.TypeConverter
4 | import java.util.UUID
5 |
6 | class UuidRoomTypeConverter {
7 |
8 | @TypeConverter
9 | fun toUuid(value: String?): UUID? {
10 | return value?.let { UUID.fromString(value) }
11 | }
12 |
13 | @TypeConverter
14 | fun fromUuid(uuid: UUID?): String? {
15 | return uuid?.toString()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/sasikanth/pinnit/widgets/CheckableImageView.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.widgets
2 |
3 | import android.content.Context
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 | import android.os.Parcelable.ClassLoaderCreator
7 | import android.os.Parcelable.Creator
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import android.view.accessibility.AccessibilityEvent
11 | import android.widget.Checkable
12 | import androidx.appcompat.widget.AppCompatImageView
13 | import androidx.customview.view.AbsSavedState
14 |
15 | /**
16 | * [AppCompatImageView] that implements [Checkable] interface to support
17 | * setting whether or not image is checked.
18 | */
19 | class CheckableImageView(
20 | context: Context,
21 | attrs: AttributeSet? = null
22 | ) : AppCompatImageView(context, attrs), Checkable {
23 | private val checkStateSet = intArrayOf(android.R.attr.state_checked)
24 | private var checked = false
25 | private var checkable = true
26 |
27 | override fun setChecked(checked: Boolean) {
28 | if (checkable && this.checked != checked) {
29 | this.checked = checked
30 | refreshDrawableState()
31 | sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
32 | }
33 | }
34 |
35 | override fun isChecked(): Boolean = checked
36 |
37 | override fun toggle() {
38 | isChecked = !checked
39 | }
40 |
41 | override fun onCreateDrawableState(extraSpace: Int): IntArray? {
42 | return if (checked) {
43 | View.mergeDrawableStates(
44 | super.onCreateDrawableState(extraSpace + checkStateSet.size),
45 | checkStateSet
46 | )
47 | } else {
48 | super.onCreateDrawableState(extraSpace)
49 | }
50 | }
51 |
52 | override fun onSaveInstanceState(): Parcelable {
53 | val superState = super.onSaveInstanceState()
54 | val savedState = SavedState(superState)
55 | savedState.checked = checked
56 | return savedState
57 | }
58 |
59 | override fun onRestoreInstanceState(state: Parcelable?) {
60 | if (state !is SavedState) {
61 | super.onRestoreInstanceState(state)
62 | return
63 | }
64 | super.onRestoreInstanceState(state.superState)
65 | isChecked = state.checked
66 | }
67 |
68 | class SavedState : AbsSavedState {
69 | constructor(superState: Parcelable?) : super(superState!!)
70 | constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
71 | readFromParcel(source)
72 | }
73 |
74 | internal var checked = false
75 |
76 | override fun writeToParcel(out: Parcel, flags: Int) {
77 | super.writeToParcel(out, flags)
78 | out.writeInt(if (checked) 1 else 0)
79 | }
80 |
81 | private fun readFromParcel(`in`: Parcel) {
82 | checked = `in`.readInt() == 1
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/proto/app_preferences.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "dev.sasikanth.pinnit.data.preferences";
4 | option java_multiple_files = true;
5 |
6 | message AppPreferences {
7 | enum Theme {
8 | UNSPECIFIED = 0;
9 | AUTO = 1;
10 | LIGHT = 2;
11 | DARK = 3;
12 | }
13 |
14 | // User selected app theme
15 | Theme theme = 1;
16 |
17 | bool oem_warning_dialog = 2;
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/pinnit_bottom_sheet_slide_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/pinnit_bottom_sheet_slide_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/color/about_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/button_group_background_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/color/button_group_stroke_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/color/button_group_text_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/color/color_notification_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/color_on_background_15.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/color/color_on_background_70.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/color_secondary_20.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/color_secondary_5.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/edit_text_stroke_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/color/material_button_background_tint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/material_button_text_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/schedule_checkbox_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/color/schedule_indicator_icon_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/schedule_indicator_stroke_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/color/schedule_indicator_text_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/pinnit_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/drawable-nodpi/pinnit_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v26/ic_shortcut_create.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/about_item_ripple.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/asld_pin_unpin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bottom_bar_item_ripple.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_drop_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
13 |
16 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground_themed.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_about.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_add.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
17 |
21 |
22 |
26 |
33 |
39 |
46 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_app_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_dark_mode.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_date.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_delete.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_email.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_notification.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_pin.xml:
--------------------------------------------------------------------------------
1 |
7 |
13 |
16 |
23 |
30 |
36 |
37 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_pinned.xml:
--------------------------------------------------------------------------------
1 |
7 |
13 |
16 |
23 |
30 |
35 |
38 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_source_code.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_warning.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pinnit_warning_80dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_qs_app_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_shortcut_create.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_shortcut_create_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_shortcut_create_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/illustration_pinnit_no_notifications.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/illustration_pinnit_pin.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/notification_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rectangle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/font/bai_jam_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/font/bai_jam_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/bai_jam_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/font/bai_jam_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/bai_jam_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/font/bai_jam_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/bai_jam_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/font/bai_jam_semibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/jura_bold.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/font/jura_semibold.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pinnit_bottom_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
23 |
24 |
39 |
40 |
53 |
54 |
69 |
70 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pinnit_oem_warning_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
28 |
29 |
39 |
40 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/theme_selection_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
18 |
19 |
26 |
27 |
35 |
36 |
37 |
38 |
51 |
52 |
59 |
60 |
67 |
68 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/notification_schedule.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/main_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
16 |
21 |
26 |
31 |
36 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 32dp
5 | 32dp
6 | 144dp
7 | 144dp
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/maroon_600
4 | @color/maroon_700
5 |
6 | @color/maroon_200
7 | @android:color/black
8 |
9 | @color/maroon_800
10 | @android:color/white
11 |
12 | @color/maroon_900
13 | @android:color/white
14 |
15 | #6614000E
16 |
17 |
18 | @color/maroon_150
19 |
20 | #CC000000
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/sys_ui.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | false
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Notificações
4 | Sem notificações
5 | Título
6 | Descrição
7 | Notificações fixadas
8 | Notificação deletada
9 |
10 | Criar
11 | Editar
12 |
13 | Criar
14 | Salvar e fixar
15 | Salvar
16 |
17 | Lembrete
18 | Repetir a cada…
19 | Dia
20 | Semana
21 | Mês
22 |
23 | %1$s - %2$s
24 | Hoje - %1$s
25 | Ontem - %1$s
26 | Amanhã - %1$s
27 | Editar lembrete
28 | Remover lembrete
29 | Este lembrete está no passado. Delete ou edite-o para uma data futura.
30 |
31 | Modo noturno
32 |
33 |
34 | Ativado
35 | Auto
36 | Desativado
37 |
38 |
39 | Sobre
40 | Política de privacidade
41 | Questões?
42 | Contatar suporte
43 | Aviso sobre seu aparelho
44 | Enviar email
45 | Sim
46 | Não
47 | Desfazer
48 |
49 |
50 | Desafixar
51 | Deletar
52 |
53 |
54 | Descartar mudanças?
55 | Sim
56 | Não
57 | Deletar notificação?
58 | Essa ação não pode ser desfeita.
59 | Sim
60 | Não
61 |
62 |
63 | Abrir app
64 |
65 |
66 | Aviso sobre o seu aparelho
67 |
68 | Algumas fabricantes são conhecidas por fechar aplicativos de maneira agressiva. Pinnit não roda em segundo plano, mas se for fechado pelo sistema, notificações vindas de Pinnit serão retiradas do seu painel e só retornarão quando o app for lançado novamente.
69 | \n
70 | \nPara evitar isto, visite o link abaixo e siga as instruções.
71 |
72 | MOSTRAR INSTRUÇÕES
73 | VERSÃO %1$s
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v27/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v27/theme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - pin $text
5 | - add $text
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #1F0014
5 | #380025
6 | #4D0033
7 | #660044
8 | #852163
9 | #A64B87
10 | #C787B1
11 | #E5B3D4
12 | #F5D0E9
13 | #FFE5F6
14 |
15 | @color/maroon_700
16 | @color/maroon_800
17 |
18 | @android:color/white
19 | @color/maroon_100
20 |
21 | @color/maroon_700
22 | @android:color/white
23 |
24 | @android:color/white
25 | @color/maroon_900
26 |
27 | @android:color/white
28 | @color/maroon_900
29 |
30 | #9C1664
31 | @android:color/white
32 |
33 | #B3FFFFFF
34 |
35 |
36 | @color/maroon_700
37 |
38 |
39 | #9C1664
40 |
41 | @color/color_on_background_15
42 | @color/color_on_background_70
43 |
44 | #80000000
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 56dp
4 | 2dp
5 |
6 |
7 | 144dp
8 | 144dp
9 | 32dp
10 | 32dp
11 |
12 |
13 | 72dp
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/floats.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - 0.4
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @array/com_google_android_gms_fonts_certs_dev
5 | - @array/com_google_android_gms_fonts_certs_prod
6 |
7 |
8 | -
9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
10 |
11 |
12 |
13 | -
14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/pinnit_bottom_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/sys_ui.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml-v25/shortcuts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/actions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/CanaryTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 |
6 | class CanaryTest {
7 | @Test
8 | fun canaryTest() {
9 | assertThat(true).isTrue()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/editor/EditorScreenInitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import com.spotify.mobius.test.FirstMatchers.hasEffects
4 | import com.spotify.mobius.test.FirstMatchers.hasModel
5 | import com.spotify.mobius.test.FirstMatchers.hasNoEffects
6 | import com.spotify.mobius.test.InitSpec
7 | import com.spotify.mobius.test.InitSpec.assertThatFirst
8 | import dev.sasikanth.sharedtestcode.TestData
9 | import org.junit.Test
10 | import java.util.UUID
11 |
12 | class EditorScreenInitTest {
13 |
14 | private val initSpec = InitSpec(EditorScreenInit())
15 | private val notificationUuid = UUID.fromString("97f6ee65-b2c6-403f-97aa-ca45ebfa444b")
16 |
17 | @Test
18 | fun `when screen is created and notification uuid is present, then fetch notification`() {
19 | val defaultModel = EditorScreenModel.default(notificationUuid, null, null)
20 |
21 | initSpec
22 | .whenInit(defaultModel)
23 | .then(
24 | assertThatFirst(
25 | hasModel(defaultModel),
26 | hasEffects(LoadNotification(notificationUuid) as EditorScreenEffect)
27 | )
28 | )
29 | }
30 |
31 | @Test
32 | fun `when screen is created and notification uuid is not present and title and content is null, then set empty title and content`() {
33 | val defaultModel = EditorScreenModel.default(null, null, null)
34 |
35 | initSpec
36 | .whenInit(defaultModel)
37 | .then(
38 | assertThatFirst(
39 | hasModel(defaultModel),
40 | hasEffects(SetTitleAndContent(defaultModel.title, defaultModel.content) as EditorScreenEffect)
41 | )
42 | )
43 | }
44 |
45 | @Test
46 | fun `when screen is restored and notification is already loaded, then do nothing`() {
47 | val notification = dev.sasikanth.sharedtestcode.TestData.notification(
48 | uuid = notificationUuid,
49 | title = "Notification Title"
50 | )
51 |
52 | val defaultModel = EditorScreenModel.default(notificationUuid, null, null)
53 | .notificationLoaded(notification)
54 | .titleChanged(notification.title)
55 |
56 | initSpec
57 | .whenInit(defaultModel)
58 | .then(
59 | assertThatFirst(
60 | hasModel(defaultModel),
61 | hasNoEffects()
62 | )
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/editor/ScheduleValidatorTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.editor
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import dev.sasikanth.pinnit.editor.ScheduleValidator.Result.ScheduleInPastError
5 | import dev.sasikanth.pinnit.editor.ScheduleValidator.Result.Valid
6 | import dev.sasikanth.sharedtestcode.utils.TestUserClock
7 | import org.junit.Test
8 | import java.time.Instant
9 | import java.time.LocalDate
10 | import java.time.LocalTime
11 |
12 | class ScheduleValidatorTest {
13 |
14 | private val userClock = dev.sasikanth.sharedtestcode.utils.TestUserClock(Instant.parse("2020-01-01T00:00:00.00Z"))
15 | private val validator = ScheduleValidator(userClock)
16 |
17 | @Test
18 | fun `when schedule is in future, then return schedule is valid`() {
19 | // given
20 | val scheduleDate = LocalDate.parse("2020-01-01")
21 | val scheduleTime = LocalTime.parse("09:00:00")
22 |
23 | // when
24 | val expectedResult = validator.validate(scheduleDate, scheduleTime)
25 |
26 | // then
27 | assertThat(expectedResult).isEqualTo(Valid)
28 | }
29 |
30 | @Test
31 | fun `when schedule is in past, then return schedule in past error`() {
32 | // given
33 | val scheduleDate = LocalDate.parse("2019-12-31")
34 | val scheduleTime = LocalTime.parse("09:00:00")
35 |
36 | // when
37 | val expectedResult = validator.validate(scheduleDate, scheduleTime)
38 |
39 | // then
40 | assertThat(expectedResult).isEqualTo(ScheduleInPastError)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import com.spotify.mobius.test.FirstMatchers.hasEffects
4 | import com.spotify.mobius.test.FirstMatchers.hasModel
5 | import com.spotify.mobius.test.FirstMatchers.hasNoEffects
6 | import com.spotify.mobius.test.InitSpec
7 | import com.spotify.mobius.test.InitSpec.assertThatFirst
8 | import dev.sasikanth.sharedtestcode.TestData
9 | import org.junit.Test
10 | import java.util.UUID
11 |
12 | class NotificationsScreenInitTest {
13 |
14 | private val initSpec = InitSpec(NotificationsScreenInit())
15 | private val defaultModel = NotificationsScreenModel.default()
16 |
17 | @Test
18 | fun `when screen is created, then load notifications`() {
19 | initSpec
20 | .whenInit(defaultModel)
21 | .then(
22 | assertThatFirst(
23 | hasModel(defaultModel),
24 | hasEffects(LoadNotifications, CheckNotificationsVisibility, CheckPermissionToPostNotification)
25 | )
26 | )
27 | }
28 |
29 | @Test
30 | fun `when screen is restored, then don't load notifications`() {
31 | val notifications = listOf(
32 | dev.sasikanth.sharedtestcode.TestData.notification(
33 | uuid = UUID.fromString("c59f2db1-2cfc-476c-9fa2-1fac99bd7336")
34 | )
35 | )
36 | val restoredModel = defaultModel
37 | .onNotificationsLoaded(notifications)
38 |
39 | initSpec
40 | .whenInit(restoredModel)
41 | .then(
42 | assertThatFirst(
43 | hasModel(restoredModel),
44 | hasEffects(CheckPermissionToPostNotification)
45 | )
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUiRenderTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.notifications
2 |
3 | import org.mockito.kotlin.mock
4 | import org.mockito.kotlin.verify
5 | import org.mockito.kotlin.verifyNoMoreInteractions
6 | import org.mockito.kotlin.verifyNoInteractions
7 | import dev.sasikanth.sharedtestcode.TestData
8 | import org.junit.Test
9 | import java.util.UUID
10 |
11 | class NotificationsScreenUiRenderTest {
12 |
13 | private val ui = mock()
14 | private val uiRender = NotificationsScreenUiRender(ui)
15 | private val defaultModel = NotificationsScreenModel.default()
16 |
17 | @Test
18 | fun `when notifications are being fetched, then render nothing`() {
19 | // when
20 | uiRender.render(defaultModel)
21 |
22 | // then
23 | verifyNoInteractions(ui)
24 | }
25 |
26 | @Test
27 | fun `show notifications and hide notification error if notifications are not empty`() {
28 | // given
29 | val notifications = listOf(
30 | dev.sasikanth.sharedtestcode.TestData.notification(
31 | uuid = UUID.fromString("29238d40-c5c8-44e6-94f3-9b40fd0314b3")
32 | )
33 | )
34 |
35 | // when
36 | uiRender.render(defaultModel.onNotificationsLoaded(notifications))
37 |
38 | // then
39 | verify(ui).hideNotificationsEmptyError()
40 | verify(ui).showNotifications(notifications)
41 | verifyNoMoreInteractions(ui)
42 | }
43 |
44 | @Test
45 | fun `show notifications error and hide notifications if notifications are empty`() {
46 | // when
47 | uiRender.render(defaultModel.onNotificationsLoaded(emptyList()))
48 |
49 | // then
50 | verify(ui).showNotificationsEmptyError()
51 | verify(ui).hideNotifications()
52 | verifyNoMoreInteractions(ui)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/oemwarning/OemCheckerTest.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.oemwarning
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 |
6 | class OemCheckerTest {
7 |
8 | @Test
9 | fun `should show warning for oem should work correctly`() {
10 | // given
11 | val onePlus = "oneplus"
12 |
13 | // when
14 | val shouldShowOemWarningDialog = shouldShowWarningForOEM(onePlus)
15 |
16 | // then
17 | assertThat(shouldShowOemWarningDialog).isTrue()
18 | }
19 |
20 | @Test
21 | fun `should show warning for oem should fail`() {
22 | // given
23 | val google = "google"
24 |
25 | // when
26 | val shouldShowOemWarningDialog = shouldShowWarningForOEM(google)
27 |
28 | // then
29 | assertThat(shouldShowOemWarningDialog).isFalse()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/test/java/dev/sasikanth/pinnit/utils/TestDispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.pinnit.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
5 |
6 | class TestDispatcherProvider : DispatcherProvider {
7 |
8 | private val testCoroutineDispatcher = UnconfinedTestDispatcher()
9 |
10 | override val main: CoroutineDispatcher
11 | get() = testCoroutineDispatcher
12 |
13 | override val io: CoroutineDispatcher
14 | get() = testCoroutineDispatcher
15 |
16 | override val default: CoroutineDispatcher
17 | get() = testCoroutineDispatcher
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
2 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // this is necessary to avoid the plugins to be loaded multiple times
3 | // in each subproject's classloader
4 | alias(libs.plugins.android.application).apply(false)
5 | alias(libs.plugins.android.library).apply(false)
6 | alias(libs.plugins.kotlin.android).apply(false)
7 | alias(libs.plugins.kotlin.parcelize).apply(false)
8 | alias(libs.plugins.compose.compiler).apply(false)
9 | alias(libs.plugins.androidx.navigation).apply(false)
10 | alias(libs.plugins.hilt).apply(false)
11 | alias(libs.plugins.protobuf).apply(false)
12 | alias(libs.plugins.ksp).apply(false)
13 | alias(libs.plugins.redacted).apply(false)
14 | }
15 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Sat Oct 30 17:19:03 IST 2021
14 | kotlin.code.style=official
15 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
16 | android.useAndroidX=true
17 | android.enableJetifier=true
18 | android.nonTransitiveRClass=false
19 | android.nonFinalResIds=false
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/icon.png
--------------------------------------------------------------------------------
/release/app-release.gpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/release/app-release.gpg
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ],
6 | "automergeType": "branch",
7 | "packageRules": [
8 | {
9 | "description": "Automerge minor updates",
10 | "matchUpdateTypes": ["minor", "patch"],
11 | "automerge": true
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2 |
3 | rootProject.name = "pinnit"
4 |
5 | pluginManagement {
6 | repositories {
7 | gradlePluginPortal()
8 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
9 | maven("https://oss.sonatype.org/content/repositories/snapshots")
10 | google()
11 | }
12 | }
13 |
14 | dependencyResolutionManagement {
15 | @Suppress("UnstableApiUsage")
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
20 | maven("https://jitpack.io")
21 | maven("https://oss.sonatype.org/content/repositories/snapshots")
22 | }
23 | }
24 |
25 | include(":app")
26 | include(":sharedTestCode")
27 |
--------------------------------------------------------------------------------
/sharedTestCode/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sharedTestCode/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("org.jetbrains.kotlin.android")
4 | }
5 |
6 | android {
7 | namespace = "dev.sasikanth.sharedtestcode"
8 | compileSdk = libs.versions.sdk.compile.get().toInt()
9 |
10 | defaultConfig {
11 | minSdk = libs.versions.sdk.min.get().toInt()
12 |
13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
14 | }
15 |
16 | buildTypes {
17 | release {
18 | isMinifyEnabled = false
19 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
20 | }
21 | }
22 | compileOptions {
23 | isCoreLibraryDesugaringEnabled = true
24 |
25 | sourceCompatibility = JavaVersion.VERSION_17
26 | targetCompatibility = JavaVersion.VERSION_17
27 | }
28 | kotlinOptions {
29 | jvmTarget = JavaVersion.VERSION_17.toString()
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation(projects.app)
35 |
36 | coreLibraryDesugaring(libs.desugar.jdk)
37 |
38 | testImplementation(libs.junit)
39 | androidTestImplementation(libs.androidx.junit)
40 | androidTestImplementation(libs.espresso)
41 | }
42 |
--------------------------------------------------------------------------------
/sharedTestCode/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msasikanth/pinnit/0cf29aa1317056ff15ccc269e437e42d8da6d80f/sharedTestCode/consumer-rules.pro
--------------------------------------------------------------------------------
/sharedTestCode/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sharedTestCode/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sharedTestCode/src/main/java/dev/sasikanth/sharedtestcode/TestData.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.sharedtestcode
2 |
3 | import dev.sasikanth.pinnit.data.PinnitNotification
4 | import dev.sasikanth.pinnit.data.Schedule
5 | import dev.sasikanth.pinnit.data.ScheduleType
6 | import java.time.Instant
7 | import java.time.LocalDate
8 | import java.time.LocalTime
9 | import java.util.UUID
10 |
11 | object TestData {
12 | fun notification(
13 | uuid: UUID = UUID.randomUUID(),
14 | title: String = "Notification Title",
15 | content: String? = null,
16 | isPinned: Boolean = false,
17 | createdAt: Instant = Instant.now(),
18 | updatedAt: Instant = Instant.now(),
19 | deletedAt: Instant? = null,
20 | scheduleDate: LocalDate = LocalDate.now(),
21 | scheduleTime: LocalTime = LocalTime.now(),
22 | scheduleType: ScheduleType? = ScheduleType.Daily,
23 | schedule: Schedule? = schedule(
24 | scheduleDate = scheduleDate,
25 | scheduleTime = scheduleTime,
26 | scheduleType = scheduleType
27 | )
28 | ): PinnitNotification {
29 | return PinnitNotification(
30 | uuid = uuid,
31 | title = title,
32 | content = content,
33 | isPinned = isPinned,
34 | createdAt = createdAt,
35 | updatedAt = updatedAt,
36 | deletedAt = deletedAt,
37 | schedule = schedule
38 | )
39 | }
40 |
41 | fun schedule(
42 | scheduleDate: LocalDate = LocalDate.now(),
43 | scheduleTime: LocalTime = LocalTime.now(),
44 | scheduleType: ScheduleType? = ScheduleType.Daily,
45 | ): Schedule {
46 | return Schedule(
47 | scheduleDate = scheduleDate,
48 | scheduleTime = scheduleTime,
49 | scheduleType = scheduleType
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/sharedTestCode/src/main/java/dev/sasikanth/sharedtestcode/utils/TestUserClock.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.sharedtestcode.utils
2 |
3 | import dev.sasikanth.pinnit.utils.UserClock
4 | import java.time.Clock
5 | import java.time.Duration
6 | import java.time.Instant
7 | import java.time.LocalDate
8 | import java.time.ZoneId
9 | import java.time.ZoneOffset
10 | import java.time.ZoneOffset.UTC
11 |
12 | class TestUserClock(instant: Instant) : UserClock() {
13 |
14 | constructor() : this(Instant.EPOCH)
15 |
16 | constructor(localDate: LocalDate, zoneOffset: ZoneOffset = UTC) : this(instantFromDateAtZone(localDate, zoneOffset))
17 |
18 | private var clock = fixed(instant, UTC)
19 |
20 | override fun withZone(zone: ZoneId): Clock = clock.withZone(zone)
21 |
22 | override fun getZone(): ZoneId = clock.zone
23 |
24 | override fun instant(): Instant = clock.instant()
25 |
26 | fun advanceBy(duration: Duration) {
27 | clock = offset(clock, duration)
28 | }
29 |
30 | fun setDate(date: LocalDate, zone: ZoneId = this.zone) {
31 | val instant = instantFromDateAtZone(date, zone)
32 | clock = fixed(instant, zone)
33 | }
34 | }
35 |
36 | private fun instantFromDateAtZone(localDate: LocalDate, zone: ZoneId): Instant {
37 | return localDate.atStartOfDay(zone).toInstant()
38 | }
39 |
--------------------------------------------------------------------------------
/sharedTestCode/src/main/java/dev/sasikanth/sharedtestcode/utils/TestUtcClock.kt:
--------------------------------------------------------------------------------
1 | package dev.sasikanth.sharedtestcode.utils
2 |
3 | import dev.sasikanth.pinnit.utils.UtcClock
4 | import java.time.Clock
5 | import java.time.Duration
6 | import java.time.Instant
7 | import java.time.LocalDate
8 | import java.time.ZoneId
9 | import java.time.ZoneOffset.UTC
10 |
11 | class TestUtcClock(instant: Instant) : UtcClock() {
12 |
13 | constructor() : this(Instant.EPOCH)
14 |
15 | private var clock = fixed(instant, UTC)
16 |
17 | override fun withZone(zoneId: ZoneId?): Clock = clock.withZone(zoneId)
18 |
19 | override fun getZone(): ZoneId = UTC
20 |
21 | override fun instant(): Instant = clock.instant()
22 |
23 | fun setDate(date: LocalDate) {
24 | val instant = date.atStartOfDay(zone).toInstant()
25 | clock = fixed(instant, UTC)
26 | }
27 |
28 | fun advanceBy(duration: Duration) {
29 | clock = offset(clock, duration)
30 | }
31 |
32 | fun resetToEpoch() {
33 | clock = fixed(Instant.EPOCH, UTC)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------