├── .android2po ├── .gitignore ├── LICENSE ├── README.md ├── Telemetry.md ├── app ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro ├── schemas │ └── org.mozilla.scryer.persistence.ScreenshotDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── mozilla │ │ └── scryer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── mozilla │ │ │ └── scryer │ │ │ ├── AdjustHelper.kt │ │ │ ├── Event.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NavHostInsetAwareFragment.kt │ │ │ ├── ScryerApplication.kt │ │ │ ├── ScryerService.kt │ │ │ ├── SvgViewerActivity.kt │ │ │ ├── capture │ │ │ ├── RequestCaptureActivity.kt │ │ │ ├── ScreenCaptureListener.kt │ │ │ └── ScreenCaptureManager.kt │ │ │ ├── collectionview │ │ │ ├── CollectionFragment.kt │ │ │ ├── ListSelector.kt │ │ │ ├── ScreenshotAdapter.kt │ │ │ └── SortingPanelDialog.kt │ │ │ ├── detailpage │ │ │ ├── DetailPageActivity.kt │ │ │ ├── DetailPageAdapter.kt │ │ │ ├── GraphicOverlay.kt │ │ │ ├── GraphicOverlayHelper.kt │ │ │ ├── GraphicOverlayTouchHelper.kt │ │ │ ├── TextBlockGraphic.kt │ │ │ ├── TextGraphic.kt │ │ │ └── TextSelectionCallback.kt │ │ │ ├── extension │ │ │ ├── FloatExtension.kt │ │ │ ├── NavigationExtension.kt │ │ │ └── ViewHolderExtension.kt │ │ │ ├── filemonitor │ │ │ ├── FileMonitor.kt │ │ │ ├── FileObserverDelegate.kt │ │ │ ├── MediaProviderDelegate.kt │ │ │ └── ScreenshotFetcher.kt │ │ │ ├── landingpage │ │ │ ├── HomeFragment.kt │ │ │ ├── MainAdapter.kt │ │ │ ├── NewScreenshotBadge.kt │ │ │ └── QuickAccessAdapter.kt │ │ │ ├── notification │ │ │ └── ScryerMessagingService.kt │ │ │ ├── overlay │ │ │ ├── CaptureButtonController.kt │ │ │ ├── Dock.kt │ │ │ ├── DragHelper.kt │ │ │ ├── Dragger.kt │ │ │ ├── FloatingView.kt │ │ │ ├── OverlayPermission.kt │ │ │ ├── Screen.kt │ │ │ └── WindowController.kt │ │ │ ├── permission │ │ │ ├── PermissionFlow.kt │ │ │ ├── PermissionHelper.kt │ │ │ └── PermissionViewModel.kt │ │ │ ├── persistence │ │ │ ├── CollectionDao.kt │ │ │ ├── CollectionModel.kt │ │ │ ├── FtsEntity.kt │ │ │ ├── LoadingViewModel.kt │ │ │ ├── ScreenshotContentModel.kt │ │ │ ├── ScreenshotDao.kt │ │ │ ├── ScreenshotDatabase.kt │ │ │ ├── ScreenshotModel.kt │ │ │ └── SuggestCollectionHelper.kt │ │ │ ├── preference │ │ │ └── PreferenceWrapper.kt │ │ │ ├── promote │ │ │ ├── PromoteDialogHelper.kt │ │ │ ├── PromoteRatingHelper.kt │ │ │ ├── PromoteShareHelper.kt │ │ │ └── Promoter.kt │ │ │ ├── repository │ │ │ ├── ScreenshotDatabaseRepository.kt │ │ │ ├── ScreenshotInMemoryRepository.kt │ │ │ └── ScreenshotRepository.kt │ │ │ ├── scan │ │ │ ├── BackgroundScanner.kt │ │ │ ├── ContentScanner.kt │ │ │ ├── FirebaseVisionTextHelper.kt │ │ │ ├── ForegroundAndBackgroundCharging.kt │ │ │ └── ForegroundScanner.kt │ │ │ ├── search │ │ │ ├── FullTextSearchFragment.kt │ │ │ ├── SearchAdapter.kt │ │ │ └── SearchFragment.kt │ │ │ ├── setting │ │ │ ├── AboutFragment.kt │ │ │ ├── PreferenceSettingsRepository.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SettingsRepository.kt │ │ │ ├── TelemetrySwitchPreference.kt │ │ │ └── YourRightsFragment.kt │ │ │ ├── sortingpanel │ │ │ ├── SortingPanel.kt │ │ │ ├── SortingPanelActivity.kt │ │ │ └── SortingPanelAdapter.kt │ │ │ ├── telemetry │ │ │ ├── CaptureServiceHeartbeatWorker.kt │ │ │ ├── FirebaseEvent.kt │ │ │ └── TelemetryWrapper.kt │ │ │ ├── ui │ │ │ ├── BottomDialog.kt │ │ │ ├── CollectionNameDialog.kt │ │ │ ├── ConfirmationDialog.kt │ │ │ ├── GridItemDecoration.kt │ │ │ ├── InnerSpaceDecoration.kt │ │ │ ├── LockableViewPager.kt │ │ │ ├── ScryerSnackbar.kt │ │ │ └── ScryerToast.kt │ │ │ ├── util │ │ │ ├── CollectionListHelper.kt │ │ │ ├── ThreadUtils.kt │ │ │ └── ViewUtils.kt │ │ │ └── viewmodel │ │ │ └── ScreenshotViewModel.kt │ └── res │ │ ├── anim │ │ ├── slid_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── color │ │ └── primary_text_button.xml │ │ ├── drawable-hdpi │ │ └── image_logotype.png │ │ ├── drawable-mdpi │ │ └── image_logotype.png │ │ ├── drawable-xhdpi │ │ └── image_logotype.png │ │ ├── drawable-xxhdpi │ │ ├── ic_stat_notify.png │ │ ├── image_appbeta.png │ │ ├── image_emptypage.png │ │ ├── image_error.png │ │ ├── image_feedback.png │ │ ├── image_logotype.png │ │ ├── image_noresult.png │ │ ├── image_search.png │ │ ├── image_searchhint.png │ │ ├── image_share.png │ │ ├── image_welcome.png │ │ ├── img_ocrhint.png │ │ └── img_pointer.png │ │ ├── drawable │ │ ├── add.xml │ │ ├── back.xml │ │ ├── bottom_dialog_bkg.xml │ │ ├── capture.xml │ │ ├── capture_button_bkg.xml │ │ ├── capture_button_exit_bkg.xml │ │ ├── capture_button_exit_border.xml │ │ ├── check.xml │ │ ├── circle_2dp_grey50.xml │ │ ├── circle_2dp_white.xml │ │ ├── close_large.xml │ │ ├── close_small.xml │ │ ├── collection_name_dialog_cursor.xml │ │ ├── contained_button_bkg.xml │ │ ├── copy.xml │ │ ├── debug_broken_svg.xml │ │ ├── detailpage_toolbar_background.xml │ │ ├── edit_close.xml │ │ ├── error.xml │ │ ├── fab_ocr.xml │ │ ├── hint.xml │ │ ├── home_new_collection_item_bkg.xml │ │ ├── home_quick_access_empty_view_border.xml │ │ ├── ic_stat_notify_sort.xml │ │ ├── image_emptyfolder.xml │ │ ├── image_noaccess.xml │ │ ├── more.xml │ │ ├── move.xml │ │ ├── outlined_button_bkg.xml │ │ ├── rect_2dp.xml │ │ ├── rect_3dp.xml │ │ ├── rect_4dp.xml │ │ ├── screenshot_select_checkbox.xml │ │ ├── search.xml │ │ ├── selected.xml │ │ ├── share.xml │ │ ├── sorted_collection_item_bkg.xml │ │ ├── sorting_panel_create_bkg.xml │ │ ├── sorting_panel_hint_bar.xml │ │ ├── sorting_panel_item_ripple.xml │ │ ├── sorting_panel_suggest_item_bkg.xml │ │ ├── trash.xml │ │ ├── unsorted_collection_item_bkg.xml │ │ ├── viewmore.xml │ │ └── websearch.xml │ │ ├── layout │ │ ├── activity_detail_page.xml │ │ ├── activity_main.xml │ │ ├── activity_settings.xml │ │ ├── activity_sorting_panel.xml │ │ ├── activity_svg_viewer.xml │ │ ├── dialog_bottom.xml │ │ ├── dialog_collection_info.xml │ │ ├── dialog_collection_name.xml │ │ ├── dialog_confirmation.xml │ │ ├── dialog_give_feedback.xml │ │ ├── dialog_promote.xml │ │ ├── dialog_screenshot_info.xml │ │ ├── fragment_about.xml │ │ ├── fragment_collection.xml │ │ ├── fragment_full_text_search.xml │ │ ├── fragment_home.xml │ │ ├── fragment_search_empty.xml │ │ ├── fragment_your_rights.xml │ │ ├── home_collection_item_container.xml │ │ ├── item_collection.xml │ │ ├── item_loading.xml │ │ ├── item_new_collection.xml │ │ ├── item_quick_access.xml │ │ ├── item_quick_access_more.xml │ │ ├── item_screenshot.xml │ │ ├── preference_screenshot_feature_sub_item.xml │ │ ├── preference_telemetry_item.xml │ │ ├── view_capture_button.xml │ │ ├── view_capture_button_exit.xml │ │ ├── view_custom_toast.xml │ │ ├── view_home_section_title.xml │ │ ├── view_home_toolbar.xml │ │ ├── view_quick_access.xml │ │ ├── view_sorting_panel.xml │ │ ├── view_sorting_panel_item.xml │ │ ├── view_storage_permission.xml │ │ └── view_welcome.xml │ │ ├── menu │ │ ├── menu_collection.xml │ │ ├── menu_collection_view_select_action_mode.xml │ │ ├── menu_detail.xml │ │ ├── menu_detailpage_text_selection.xml │ │ ├── menu_main.xml │ │ └── menu_sorting_panel.xml │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi-v26 │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi-v26 │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi-v26 │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi-v26 │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── raw │ │ └── image_notification.json │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-land │ │ └── integers.xml │ │ ├── values-v23 │ │ ├── colors.xml │ │ └── styles.xml │ │ ├── values-v24 │ │ └── colors.xml │ │ ├── values-v28 │ │ └── styles.xml │ │ ├── values │ │ ├── app.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── integers.xml │ │ ├── preference_keys.xml │ │ ├── strings.xml │ │ ├── strings_your_rights.xml │ │ ├── styles.xml │ │ └── styles_text.xml │ │ └── xml │ │ ├── fileprovider.xml │ │ ├── searchable.xml │ │ └── settings.xml │ ├── preview │ └── res │ │ ├── mipmap │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── org │ └── mozilla │ └── scryer │ ├── ExampleUnitTest.java │ ├── permission │ └── PermissionFlowTest.kt │ └── util │ └── CollectionListHelperTest.kt ├── build.gradle ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Dependencies.kt ├── components └── service │ ├── telemetry-annotation │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── mozilla │ │ └── telemetry │ │ └── annotation │ │ ├── TelemetryDoc.kt │ │ └── TelemetryExtra.java │ └── telemetry-compiler │ ├── build.gradle │ └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── mozilla │ │ │ └── telemetry │ │ │ └── compiler │ │ │ └── TelemetryAnnotationProcessor.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── javax.annotation.processing.Processor │ └── test │ └── java │ └── org │ └── mozilla │ └── telemetry │ └── compiler │ └── TelemetryAnnotationProcessorTest.java ├── docs └── events.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools └── l10n ├── android2po ├── README.md ├── a2po.py ├── commands.py ├── commands.pyc ├── config.py ├── config.pyc ├── convert.py ├── convert.pyc ├── env.py ├── env.pyc ├── program.py ├── program.pyc ├── requirements.txt ├── utils.py └── utils.pyc ├── check_translations.py ├── create_commits.sh ├── export-strings.sh ├── fix_locale_folders.sh └── import-strings.sh /.android2po: -------------------------------------------------------------------------------- 1 | # Configuration for android2po for converting strings from Android String XML to Gettext PO files and back 2 | 3 | --android app/src/main/res 4 | --groups strings 5 | 6 | # That's where we store our exported po files. This directory is added to .gitignore and should not be 7 | # commited to the Focus repository. Usually this is a checkout of the l10n repository: 8 | # https://github.com/mozilla-l10n/scryer-android-l10n 9 | --gettext l10n-repo/ 10 | 11 | # This is following the layout that Pontoon requires: 12 | # https://developer.mozilla.org/en-US/docs/Mozilla/Implementing_Pontoon_in_a_Mozilla_website 13 | --layout locales/%(locale)s/app.po 14 | 15 | --template locales/templates/app.pot 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | # Uncomment the following line if you do not want to check your keystore files in. 41 | #*.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | # fastlane 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots 58 | fastlane/test_output 59 | fastlane/readme.md 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project-ScreenshotGo 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # We do not want to obfuscate - It's just painful to debug without the right mapping file. 24 | -dontobfuscate 25 | 26 | # Should remove this flag after updating support libraries to 28.x.x 27 | # https://github.com/googlecodelabs/android-workmanager/issues/34 28 | -ignorewarnings 29 | 30 | # These fragment are referenced from navigation graph xml, so it's not retained during minify. 31 | # This rule is a little too broad, because we might not need all the fragment in difference flavor 32 | -keep class org.mozilla.scryer.** extends android.support.v4.app.Fragment{} 33 | 34 | # Adjust 35 | -keep public class com.adjust.sdk.** { *; } 36 | -keep class com.google.android.gms.common.ConnectionResult { 37 | int SUCCESS; 38 | } 39 | -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { 40 | com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context); 41 | } 42 | -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { 43 | java.lang.String getId(); 44 | boolean isLimitAdTrackingEnabled(); 45 | } 46 | -keep public class com.android.installreferrer.** { *; } 47 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/mozilla/scryer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("org.mozilla.scryer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/AdjustHelper.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer 7 | 8 | import android.app.Activity 9 | import android.app.Application 10 | import android.os.Bundle 11 | import com.adjust.sdk.Adjust 12 | import com.adjust.sdk.AdjustConfig 13 | import com.adjust.sdk.AdjustEvent 14 | import com.adjust.sdk.LogLevel 15 | 16 | const val ADJUST_EVENT_CAPTURE_VIA_FAB = "ltd7wr" 17 | const val ADJUST_EVENT_FEEDBACK_POSITIVE = "i7wmh5" 18 | const val ADJUST_EVENT_SHARE_APP = "er82lg" 19 | const val ADJUST_EVENT_SORT_SCREENSHOT = "3odfiz" 20 | const val ADJUST_EVENT_START_SEARCH = "g6icdf" 21 | const val ADJUST_EVENT_VIEW_TEXT_IN_SCREENSHOT = "t7ubav" 22 | 23 | class AdjustHelper { 24 | companion object { 25 | fun init(application: Application) { 26 | val token = BuildConfig.ADJUST_TOKEN.takeIf { it.isNotEmpty() } ?: return 27 | 28 | val config = AdjustConfig(application, token, BuildConfig.ADJUST_ENVIRONMENT, true) 29 | config.setSendInBackground(true) 30 | config.setLogLevel(LogLevel.DEBUG) 31 | Adjust.onCreate(config) 32 | 33 | application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { 34 | override fun onActivityPaused(activity: Activity?) { 35 | Adjust.onPause() 36 | } 37 | 38 | override fun onActivityResumed(activity: Activity?) { 39 | Adjust.onResume() 40 | } 41 | 42 | override fun onActivityStarted(activity: Activity?) { 43 | } 44 | 45 | override fun onActivityDestroyed(activity: Activity?) { 46 | } 47 | 48 | override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { 49 | } 50 | 51 | override fun onActivityStopped(activity: Activity?) { 52 | } 53 | 54 | override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { 55 | } 56 | }) 57 | } 58 | 59 | fun trackEvent(eventToken: String) { 60 | Adjust.trackEvent(AdjustEvent(eventToken)) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/Event.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer 7 | 8 | import androidx.lifecycle.Observer 9 | 10 | open class Event(private val content: T) { 11 | private var handled = false 12 | 13 | fun getUnhandledContent(): T? { 14 | return if (handled) { 15 | null 16 | } else { 17 | handled = true 18 | content 19 | } 20 | } 21 | } 22 | 23 | class EventObserver(private val onEvent: (T) -> Unit) : Observer> { 24 | override fun onChanged(t: Event?) { 25 | t?.getUnhandledContent()?.let(onEvent) 26 | } 27 | } 28 | 29 | class Observer(private val onEvent: (T) -> Unit) : Observer { 30 | override fun onChanged(t: T?) { 31 | t?.let { 32 | onEvent(it) 33 | } 34 | } 35 | } 36 | 37 | abstract class NonNullObserver : Observer { 38 | override fun onChanged(t: T?) { 39 | t?.let { 40 | onValueChanged(it) 41 | } 42 | } 43 | 44 | abstract fun onValueChanged(newValue: T) 45 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/NavHostInsetAwareFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package org.mozilla.scryer 8 | 9 | import android.os.Bundle 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.view.WindowInsets 14 | import android.widget.FrameLayout 15 | import androidx.navigation.fragment.NavHostFragment 16 | 17 | class NavHostInsetAwareFragment : NavHostFragment() { 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 19 | val frameLayout = object : FrameLayout(inflater.context) { 20 | var lastInsets: WindowInsets? = null 21 | 22 | override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets { 23 | if (lastInsets != insets) { 24 | lastInsets = insets 25 | requestLayout() 26 | } 27 | return insets?.consumeSystemWindowInsets() ?: super.onApplyWindowInsets(insets) 28 | } 29 | 30 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 31 | lastInsets?.let { insets -> 32 | (0 until childCount).map { getChildAt(it) }.forEach { child -> 33 | child.dispatchApplyWindowInsets(insets) 34 | } 35 | } 36 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 37 | } 38 | } 39 | frameLayout.id = this.id 40 | return frameLayout 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ScryerApplication.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer 7 | 8 | import android.app.Application 9 | import mozilla.components.support.base.log.Log 10 | import mozilla.components.support.base.log.sink.AndroidLogSink 11 | import org.mozilla.scryer.repository.ScreenshotRepository 12 | import org.mozilla.scryer.scan.ContentScanner 13 | import org.mozilla.scryer.scan.ForegroundAndBackgroundCharging 14 | import org.mozilla.scryer.setting.PreferenceSettingsRepository 15 | import org.mozilla.scryer.setting.SettingsRepository 16 | import org.mozilla.scryer.telemetry.TelemetryWrapper 17 | 18 | class ScryerApplication : Application() { 19 | companion object { 20 | private val instance: ScryerApplication by lazy { 21 | ApplicationHolder.instance 22 | } 23 | 24 | fun getScreenshotRepository(): ScreenshotRepository { 25 | return instance.screenshotRepository 26 | } 27 | 28 | fun getSettingsRepository(): SettingsRepository { 29 | return instance.settingsRepository 30 | } 31 | 32 | fun getContentScanner(): ContentScanner { 33 | return instance.contentScanner 34 | } 35 | } 36 | 37 | private object ApplicationHolder { 38 | lateinit var instance: ScryerApplication 39 | } 40 | 41 | lateinit var screenshotRepository: ScreenshotRepository 42 | lateinit var settingsRepository: SettingsRepository 43 | 44 | private val contentScanner = ContentScanner() 45 | 46 | override fun onCreate() { 47 | super.onCreate() 48 | ApplicationHolder.instance = this 49 | TelemetryWrapper.init(this) 50 | AdjustHelper.init(this) 51 | Log.addSink(AndroidLogSink()) 52 | 53 | screenshotRepository = ScreenshotRepository.createRepository(this) { 54 | screenshotRepository.setupDefaultContent(this) 55 | } 56 | settingsRepository = PreferenceSettingsRepository.getInstance(this) 57 | 58 | contentScanner.onCreate(ForegroundAndBackgroundCharging()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/capture/RequestCaptureActivity.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.capture 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.media.projection.MediaProjectionManager 6 | import android.os.Bundle 7 | import android.view.WindowManager 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.content.ContextCompat 10 | 11 | class RequestCaptureActivity : AppCompatActivity() { 12 | 13 | private var requestStartTime: Long = 0 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | // Draw transparent status and navigation bar 19 | val window = this.window 20 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 21 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 22 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 23 | window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent) 24 | window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent) 25 | 26 | requestScreenCapturePermission() 27 | } 28 | 29 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 30 | if (requestCode == REQUEST_CODE_SCREEN_CAPTURE_PERMISSION) { 31 | val intent = Intent(getResultBroadcastAction(this)) 32 | intent.putExtra(RESULT_EXTRA_CODE, resultCode) 33 | intent.putExtra(RESULT_EXTRA_DATA, data) 34 | intent.putExtra(RESULT_EXTRA_PROMPT_SHOWN, promptShown()) 35 | androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(this).sendBroadcast(intent) 36 | finish() 37 | } else { 38 | super.onActivityResult(requestCode, resultCode, data) 39 | } 40 | } 41 | 42 | private fun requestScreenCapturePermission() { 43 | val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager 44 | val intent = mediaProjectionManager.createScreenCaptureIntent() 45 | 46 | requestStartTime = System.currentTimeMillis() 47 | startActivityForResult(intent, REQUEST_CODE_SCREEN_CAPTURE_PERMISSION) 48 | } 49 | 50 | private fun promptShown(): Boolean { 51 | // Assume that the prompt was shown if the response took 200ms or more to return. 52 | return System.currentTimeMillis() - requestStartTime > 200 53 | } 54 | 55 | companion object { 56 | const val RESULT_EXTRA_CODE = "code" 57 | const val RESULT_EXTRA_DATA = "data" 58 | const val RESULT_EXTRA_PROMPT_SHOWN = "prompt-shown" 59 | 60 | private const val REQUEST_CODE_SCREEN_CAPTURE_PERMISSION = 1 61 | 62 | fun getResultBroadcastAction(context: Context): String { 63 | return context.packageName + ".CAPTURE" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/capture/ScreenCaptureListener.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.capture 2 | 3 | interface ScreenCaptureListener { 4 | fun onScreenShotTaken(path: String) 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/collectionview/ListSelector.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.collectionview 7 | 8 | abstract class ListSelector { 9 | var isSelectMode = false 10 | val selected = mutableListOf() 11 | private val pendingTransition = mutableListOf() 12 | 13 | fun enterSelectionMode() { 14 | if (isSelectMode) { 15 | return 16 | } 17 | isSelectMode = true 18 | onEnterSelectMode() 19 | } 20 | 21 | fun exitSelectionMode() { 22 | isSelectMode = false 23 | pendingTransition.addAll(selected) 24 | selected.clear() 25 | onExitSelectMode() 26 | } 27 | 28 | fun toggleSelection(listItem: T) { 29 | if (selected.contains(listItem)) { 30 | selected.remove(listItem) 31 | pendingTransition.add(listItem) 32 | } else { 33 | selected.add(listItem) 34 | pendingTransition.add(listItem) 35 | } 36 | onSelectChanged() 37 | } 38 | 39 | fun isSelected(listItem: T): Boolean { 40 | return selected.contains(listItem) 41 | } 42 | 43 | fun processSelection(listItem: T, callback: (stateChanged: Boolean) -> Unit) { 44 | if (pendingTransition.contains(listItem)) { 45 | pendingTransition.remove(listItem) 46 | callback.invoke(true) 47 | } else { 48 | callback.invoke(false) 49 | } 50 | } 51 | 52 | abstract fun onEnterSelectMode() 53 | abstract fun onExitSelectMode() 54 | abstract fun onSelectChanged() 55 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/detailpage/GraphicOverlayTouchHelper.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.detailpage 6 | 7 | import android.content.Context 8 | import android.view.GestureDetector 9 | import android.view.MotionEvent 10 | 11 | class GraphicOverlayTouchHelper(context: Context, val blocks: List) { 12 | 13 | var callback: Callback? = null 14 | 15 | private val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { 16 | override fun onShowPress(e: MotionEvent?) {} 17 | 18 | override fun onSingleTapUp(e: MotionEvent): Boolean { 19 | var selected: TextBlockGraphic? = null 20 | blocks.forEach { 21 | it.isSelected = it.boundingBox.contains(e.x, e.y) 22 | if (it.isSelected) { 23 | selected = it 24 | } 25 | } 26 | selected?.let { 27 | callback?.onBlockSelectStateChanged(it) 28 | } ?: callback?.onBlockSelectStateChanged(null) 29 | return true 30 | } 31 | 32 | override fun onDown(e: MotionEvent?): Boolean { 33 | return true 34 | } 35 | 36 | override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { 37 | return false 38 | } 39 | 40 | override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean { 41 | return false 42 | } 43 | 44 | override fun onLongPress(e: MotionEvent?) { 45 | 46 | } 47 | }) 48 | 49 | fun onTouchEvent(event: MotionEvent): Boolean { 50 | return gestureDetector.onTouchEvent(event) 51 | } 52 | 53 | interface Callback { 54 | fun onBlockSelectStateChanged(block: TextBlockGraphic?) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/detailpage/TextGraphic.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.detailpage 2 | 3 | import android.graphics.* 4 | import android.util.Log 5 | 6 | import com.google.firebase.ml.vision.text.FirebaseVisionText 7 | 8 | class TextGraphic internal constructor( 9 | overlay: GraphicOverlay, 10 | private val block: FirebaseVisionText.TextBlock? 11 | ) : GraphicOverlay.Graphic(overlay) { 12 | 13 | private val rectPaint: Paint = Paint() 14 | private val textPaint: Paint = Paint() 15 | 16 | init { 17 | rectPaint.color = Color.parseColor("#ccffcc00") 18 | rectPaint.style = Paint.Style.FILL_AND_STROKE 19 | rectPaint.strokeWidth = STROKE_WIDTH 20 | 21 | textPaint.color = Color.BLACK 22 | textPaint.textSize = TEXT_SIZE 23 | // Redraw the overlay, as this graphic has been added. 24 | postInvalidate() 25 | } 26 | 27 | /** 28 | * Draws the text block annotations for position, size, and raw value on the supplied canvas. 29 | */ 30 | override fun draw(canvas: Canvas) { 31 | Log.d(TAG, "on draw text graphic") 32 | if (block == null) { 33 | throw IllegalStateException("Attempting to draw a null text.") 34 | } 35 | 36 | // Draws the bounding box around the TextBlock. 37 | val rect = RectF(block.boundingBox) 38 | canvas.drawRect(rect, rectPaint) 39 | 40 | for (line in block.lines.toMutableList().apply{ sortBy{ it.boundingBox?.centerY() } }) { 41 | line.boundingBox?.let { 42 | estimateSize(line.text, it) 43 | canvas.drawText(line.text, it.left.toFloat(), it.bottom.toFloat() /*- textPaint.fontMetrics.bottom*/, textPaint) 44 | } 45 | } 46 | } 47 | 48 | private fun estimateSize(text: String, boundingBox: Rect) { 49 | val width = boundingBox.width() 50 | 51 | var start = 1 52 | var end = 300 53 | val bound = Rect() 54 | while (start < end) { 55 | val size = start + (end - start) / 2 56 | textPaint.textSize = size.toFloat() 57 | textPaint.getTextBounds(text, 0, text.length, bound) 58 | 59 | val measureWidth = bound.right - bound.left 60 | val diffWidth = width - measureWidth 61 | if (diffWidth == 0) { 62 | return 63 | } else { 64 | if (diffWidth > 0) { 65 | start = size + 1 66 | } else { 67 | end = size - 1 68 | } 69 | } 70 | } 71 | } 72 | 73 | companion object { 74 | private const val TAG = "TextGraphic" 75 | private const val TEXT_SIZE = 54.0f 76 | private const val STROKE_WIDTH = 4.0f 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/extension/FloatExtension.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.extension 7 | 8 | import android.util.DisplayMetrics 9 | import kotlin.math.ceil 10 | import kotlin.math.floor 11 | 12 | fun Float.dpToPx(displayMetrics: DisplayMetrics): Int = (this * displayMetrics.density).round() 13 | private fun Float.round(): Int = (if (this < 0) ceil(this - 0.5f) else floor(this + 0.5f)).toInt() 14 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/extension/NavigationExtension.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.extension 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import androidx.navigation.NavController 6 | import androidx.navigation.Navigation 7 | 8 | fun NavController.navigateSafely(srcId: Int, actionId: Int, bundle: Bundle) { 9 | if (currentDestination?.id == srcId) { 10 | navigate(actionId, bundle) 11 | } 12 | } 13 | 14 | fun Fragment.getNavController(): NavController? { 15 | return view?.let { 16 | Navigation.findNavController(it) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/extension/ViewHolderExtension.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.extension 7 | 8 | import androidx.recyclerview.widget.RecyclerView 9 | 10 | fun RecyclerView.ViewHolder.getValidPosition(callback: (position: Int) -> Unit): Boolean { 11 | if (adapterPosition != RecyclerView.NO_POSITION) { 12 | callback(adapterPosition) 13 | return true 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/filemonitor/FileMonitor.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.filemonitor 7 | 8 | import android.util.Log 9 | 10 | class FileMonitor(private val delegate: FileMonitorDelegate): FileMonitorDelegate by delegate { 11 | companion object { 12 | private const val TAG = "FileMonitor" 13 | } 14 | 15 | override fun startMonitor(listener: ChangeListener) { 16 | delegate.startMonitor(object : ChangeListener { 17 | override fun onChangeStart(path: String) { 18 | Log.d(TAG, "${delegate.javaClass.simpleName} onChangeStart, $path") 19 | listener.onChangeStart(path) 20 | } 21 | 22 | override fun onChangeFinish(path: String) { 23 | Log.d(TAG, "${delegate.javaClass.simpleName} onChangeFinish, $path") 24 | listener.onChangeFinish(path) 25 | } 26 | }) 27 | } 28 | 29 | interface ChangeListener { 30 | fun onChangeStart(path: String) {} 31 | fun onChangeFinish(path: String) {} 32 | } 33 | } 34 | 35 | interface FileMonitorDelegate { 36 | fun startMonitor(listener: FileMonitor.ChangeListener) 37 | fun stopMonitor() 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/filemonitor/FileObserverDelegate.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.filemonitor 7 | 8 | import android.os.Environment 9 | import android.os.FileObserver 10 | import android.os.Handler 11 | import java.io.File 12 | 13 | class FileObserverDelegate(private val handler: Handler) : FileMonitorDelegate { 14 | 15 | private var observer: FileObserver? = null 16 | 17 | override fun startMonitor(listener: FileMonitor.ChangeListener) { 18 | val monitorDir = "${Environment.getExternalStorageDirectory()}" + 19 | File.separator + "Pictures" + 20 | File.separator + "Screenshots" 21 | observer = object : FileObserver(monitorDir, 22 | FileObserver.CREATE or FileObserver.CLOSE_WRITE) { 23 | override fun onEvent(event: Int, path: String?) { 24 | path?.apply { 25 | val filePath = monitorDir + File.separator + path 26 | val eventCode = event and FileObserver.ALL_EVENTS 27 | when (eventCode) { 28 | FileObserver.CREATE -> handler.post { listener.onChangeStart(filePath) } 29 | FileObserver.CLOSE_WRITE -> handler.post { listener.onChangeFinish(filePath) } 30 | } 31 | } 32 | } 33 | }.apply { 34 | startWatching() 35 | } 36 | } 37 | 38 | override fun stopMonitor() { 39 | observer?.stopWatching() 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/landingpage/NewScreenshotBadge.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.landingpage 6 | 7 | import android.content.Context 8 | import android.util.AttributeSet 9 | import android.widget.TextView 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.content.ContextCompat 12 | import androidx.core.graphics.drawable.DrawableCompat 13 | import org.mozilla.scryer.R 14 | 15 | class NewScreenshotBadge(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { 16 | var count: Int = 0 17 | set(value) { 18 | val coerce = value.coerceAtMost(999) 19 | field = coerce 20 | val suffix = if (value > 999) { "+" } else { "" } 21 | "$coerce$suffix".apply { text = this } 22 | } 23 | 24 | init { 25 | ContextCompat.getDrawable(context, R.drawable.rect_3dp)?.let { 26 | DrawableCompat.wrap(it.mutate()) 27 | }?.apply { 28 | DrawableCompat.setTint(this, ContextCompat.getColor(context, R.color.errorRed)) 29 | background = this 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/overlay/Dock.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.overlay 7 | 8 | open class Dock(val screen: Screen) { 9 | companion object { 10 | private const val DOCK_OFFSET_FACTOR = 0.1f 11 | } 12 | 13 | var side: Side = Side.Right 14 | private var yPositionPercentage: Float = 1 / 2f 15 | 16 | init { 17 | 18 | } 19 | 20 | open fun resolveX(targetSize: Int): Float { 21 | val offset = DOCK_OFFSET_FACTOR * targetSize 22 | return if (side == Side.Left) { 23 | (targetSize / 2) - offset 24 | } else { 25 | (screen.width - targetSize / 2) + offset 26 | } 27 | } 28 | 29 | open fun resolveY(targetSize: Int): Float { 30 | return screen.height * yPositionPercentage 31 | } 32 | 33 | open fun updatePosition(x: Int, y: Int) { 34 | yPositionPercentage = y / screen.height.toFloat() 35 | side = if (x < screen.width / 2) Dock.Side.Left else Dock.Side.Right 36 | } 37 | 38 | sealed class Side { 39 | object Left: Side() 40 | object Right: Side() 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/overlay/Dragger.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.overlay 7 | 8 | import android.content.Context 9 | import android.view.View 10 | import org.mozilla.scryer.extension.dpToPx 11 | 12 | class Dragger(private val context: Context, 13 | private val targetView: View, 14 | width: Int, 15 | height: Int, 16 | private val windowCtrl: WindowController) { 17 | 18 | private lateinit var dragView: View 19 | private lateinit var dragHelper: DragHelper 20 | 21 | private val minTouchAreaSize = 48f.dpToPx(context.resources.displayMetrics) 22 | private val dragAreaWidth = Math.max(width, minTouchAreaSize) 23 | private val dragAreaHeight = Math.max(height, minTouchAreaSize) 24 | 25 | var dragListener: DragHelper.DragListener? = null 26 | 27 | init { 28 | initDragView() 29 | } 30 | 31 | fun updatePosition() { 32 | dragView.parent?.let { 33 | val offsetX = (dragAreaWidth - targetView.width) / 2 34 | val offsetY = (dragAreaHeight - targetView.height) / 2 35 | windowCtrl.moveViewTo(dragView, targetView.x.toInt() - offsetX, targetView.y.toInt() - offsetY) 36 | } 37 | } 38 | 39 | private fun initDragView() { 40 | this.dragView = View(context) 41 | //this.dragView.setBackgroundColor(Color.parseColor("#88ff0000")) 42 | 43 | this.dragHelper = DragHelper(dragView, windowCtrl) 44 | this.dragHelper.dragListener = object : DragHelper.DragListener { 45 | override fun onTouch() { 46 | dragListener?.onTouch() 47 | } 48 | 49 | override fun onTap() { 50 | dragListener?.onTap() 51 | } 52 | 53 | override fun onLongPress() { 54 | dragListener?.onLongPress() 55 | } 56 | 57 | override fun onRelease(x: Float, y: Float) { 58 | dragListener?.onRelease(x, y) 59 | } 60 | 61 | override fun onDrag(x: Float, y: Float) { 62 | dragListener?.onDrag(x, y) 63 | } 64 | } 65 | 66 | this.dragView.setOnTouchListener(dragHelper) 67 | } 68 | 69 | fun attachToWindow() { 70 | windowCtrl.addView(dragAreaWidth, dragAreaWidth, true, dragView) 71 | val radiusX = dragAreaWidth / 2 72 | val radiusY = dragAreaHeight / 2 73 | windowCtrl.moveViewTo(dragView, targetView.x.toInt() - radiusX, targetView.y.toInt() - radiusY) 74 | } 75 | 76 | fun detachFromWindow() { 77 | windowCtrl.removeView(dragView) 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/overlay/OverlayPermission.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.overlay 7 | 8 | import android.annotation.TargetApi 9 | import android.app.AppOpsManager 10 | import android.content.Context 11 | import android.content.Intent 12 | import android.net.Uri 13 | import android.os.Build 14 | import android.provider.Settings 15 | import androidx.annotation.RequiresApi 16 | 17 | object OverlayPermission { 18 | 19 | fun hasPermission(context: Context): Boolean { 20 | return when { 21 | Build.VERSION.SDK_INT == Build.VERSION_CODES.O -> { 22 | return Settings.canDrawOverlays(context) || hasPermissionV26Workaround(context) 23 | } 24 | 25 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { 26 | Settings.canDrawOverlays(context) 27 | } 28 | 29 | else -> true 30 | } 31 | } 32 | 33 | /** 34 | * On Android O, Settings.canDrawOverlays() will keep returning false after user grant and resume 35 | * back from system setting, it will start to return true after being paused again. 36 | * In this case we use AppOpsManager to check for the permission. 37 | * 38 | * Scenarios 39 | * 1. Not grant: canDrawOverlays is false, Op is 2 (MODE_ERRORED) 40 | * 2. Grant then resume: canDrawOverlay is false, Op is 1 (MODE_IGNORED) 41 | * 3. Grant then resume then pause: canDrawOverlay is true, Op is 0 (MODE_ALLOWED) 42 | * 43 | * Reference: 44 | * https://stackoverflow.com/questions/46173460/why-in-android-o-method-settings-candrawoverlays-returns-false-when-user-has 45 | */ 46 | @TargetApi(Build.VERSION_CODES.O) 47 | private fun hasPermissionV26Workaround(context: Context): Boolean { 48 | return try { 49 | val opsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager 50 | val mode = opsManager.checkOpNoThrow("android:system_alert_window", 51 | android.os.Process.myUid(), 52 | context.packageName) 53 | mode == 0 || mode == 1 54 | 55 | } catch (e: Exception) { 56 | false 57 | } 58 | } 59 | 60 | @RequiresApi(Build.VERSION_CODES.M) 61 | fun createPermissionIntent(context: Context): Intent { 62 | return Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.packageName)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/overlay/Screen.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.overlay 7 | 8 | import android.content.Context 9 | import android.view.View 10 | import android.view.ViewGroup.LayoutParams 11 | import android.widget.RelativeLayout 12 | 13 | class Screen(context: Context, private val windowCtrl: WindowController) { 14 | private val onLayoutChangeListener = View.OnLayoutChangeListener { 15 | _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> 16 | if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { 17 | onBoundaryUpdateListener?.run() 18 | } 19 | } 20 | 21 | val containerView = object : RelativeLayout(context) { 22 | override fun onAttachedToWindow() { 23 | super.onAttachedToWindow() 24 | addOnLayoutChangeListener(onLayoutChangeListener) 25 | } 26 | 27 | override fun onDetachedFromWindow() { 28 | super.onDetachedFromWindow() 29 | removeOnLayoutChangeListener(onLayoutChangeListener) 30 | } 31 | } 32 | 33 | val width: Int 34 | get() = containerView.width 35 | 36 | val height: Int 37 | get() = containerView.height 38 | 39 | var onBoundaryUpdateListener: Runnable? = null 40 | 41 | init { 42 | windowCtrl.addView(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, false, 43 | containerView) 44 | } 45 | 46 | fun addView(view: FloatingView, width: Int, height: Int) { 47 | containerView.addView(view, width, height) 48 | } 49 | 50 | fun detachFromWindow() { 51 | windowCtrl.removeView(containerView) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/overlay/WindowController.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.overlay 7 | 8 | import android.graphics.PixelFormat 9 | import android.graphics.Point 10 | import android.os.Build 11 | import android.view.Gravity 12 | import android.view.View 13 | import android.view.WindowManager 14 | import com.crashlytics.android.Crashlytics 15 | 16 | class WindowController internal constructor(private val windowManager: WindowManager) { 17 | private val tmp: Point = Point() 18 | 19 | fun addView(width: Int, height: Int, isTouchable: Boolean, view: View) { 20 | val touchableFlag = if (isTouchable) 0 else WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 21 | 22 | val windowType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 23 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 24 | } else { 25 | @Suppress("DEPRECATION") 26 | WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 27 | } 28 | 29 | val params = WindowManager.LayoutParams(width, height, windowType, 30 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchableFlag, 31 | PixelFormat.TRANSLUCENT) 32 | 33 | params.gravity = Gravity.TOP or Gravity.START 34 | params.x = 0 35 | params.y = 0 36 | 37 | try { 38 | windowManager.addView(view, params) 39 | } catch (e: WindowManager.BadTokenException) { 40 | Crashlytics.logException(e) 41 | 42 | } catch (e: WindowManager.InvalidDisplayException) { 43 | Crashlytics.logException(e) 44 | } 45 | } 46 | 47 | fun getViewPositionX(view: View): Int { 48 | val params = view.layoutParams as WindowManager.LayoutParams 49 | return params.x 50 | } 51 | 52 | fun getViewPositionY(view: View): Int { 53 | val params = view.layoutParams as WindowManager.LayoutParams 54 | return params.y 55 | } 56 | 57 | fun removeView(view: View) { 58 | view.parent?.run { 59 | windowManager.removeView(view) 60 | } 61 | } 62 | 63 | fun moveViewTo(view: View, x: Int, y: Int) { 64 | val params = view.layoutParams as WindowManager.LayoutParams 65 | params.x = x 66 | params.y = y 67 | windowManager.updateViewLayout(view, params) 68 | } 69 | 70 | fun getWindowWidth(): Int { 71 | windowManager.defaultDisplay.getSize(tmp) 72 | return tmp.x 73 | } 74 | 75 | fun getWindowHeight(): Int { 76 | windowManager.defaultDisplay.getSize(tmp) 77 | return tmp.y 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/permission/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.permission 7 | 8 | import android.Manifest 9 | import android.annotation.TargetApi 10 | import android.app.Activity 11 | import android.content.Context 12 | import android.content.pm.PackageManager 13 | import android.os.Build 14 | import androidx.core.content.ContextCompat 15 | import androidx.fragment.app.FragmentActivity 16 | import org.mozilla.scryer.overlay.OverlayPermission 17 | 18 | class PermissionHelper { 19 | companion object { 20 | fun hasStoragePermission(context: Context) = ContextCompat.checkSelfPermission(context, 21 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED 22 | 23 | @TargetApi(Build.VERSION_CODES.M) 24 | fun shouldShowStorageRational(activity: FragmentActivity): Boolean { 25 | return activity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) 26 | } 27 | 28 | fun hasOverlayPermission(context: Context): Boolean { 29 | return Build.VERSION.SDK_INT < Build.VERSION_CODES.M 30 | || OverlayPermission.hasPermission(context) 31 | } 32 | 33 | fun requestOverlayPermission(activity: Activity?, requestCode: Int) { 34 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 35 | return 36 | } 37 | activity?.let { 38 | val intent = OverlayPermission.createPermissionIntent(it) 39 | it.startActivityForResult(intent, requestCode) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/permission/PermissionViewModel.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.permission 7 | 8 | import androidx.lifecycle.MutableLiveData 9 | import androidx.lifecycle.ViewModel 10 | import android.content.pm.PackageManager 11 | import org.mozilla.scryer.Event 12 | 13 | class PermissionViewModel : ViewModel() { 14 | val permissionRequest = PermissionLiveData() 15 | } 16 | 17 | class PermissionLiveData: MutableLiveData>() { 18 | fun notify(results: IntArray) { 19 | if (!results.isEmpty()) { 20 | value = Event(results.map { it == PackageManager.PERMISSION_GRANTED }.toBooleanArray()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/CollectionDao.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.persistence 7 | 8 | import androidx.lifecycle.LiveData 9 | import androidx.room.* 10 | import androidx.room.OnConflictStrategy.REPLACE 11 | 12 | @Dao 13 | abstract class CollectionDao { 14 | 15 | @Query("SELECT * FROM collection") 16 | abstract fun getCollections(): LiveData> 17 | 18 | @Query("SELECT * FROM collection") 19 | abstract fun getCollectionList(): List 20 | 21 | @Insert(onConflict = REPLACE) 22 | abstract fun addCollection(collection: CollectionModel) 23 | 24 | @Update(onConflict = REPLACE) 25 | abstract fun updateCollection(collection: CollectionModel) 26 | 27 | @Delete 28 | abstract fun deleteCollection(collection: CollectionModel) 29 | 30 | @Query("SELECT * FROM collection WHERE id = :id") 31 | abstract fun getCollection(id: String): CollectionModel? 32 | 33 | @Transaction 34 | open fun updateCollectionId(collection: CollectionModel, id: String) { 35 | deleteCollection(collection) 36 | collection.id = id 37 | addCollection(collection) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/CollectionModel.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.persistence 7 | 8 | import androidx.room.ColumnInfo 9 | import androidx.room.Entity 10 | import androidx.room.Ignore 11 | import androidx.room.PrimaryKey 12 | import java.util.* 13 | 14 | @Entity(tableName = "collection") 15 | data class CollectionModel constructor( 16 | @PrimaryKey(autoGenerate = false) var id: String, 17 | @ColumnInfo(name = "name") var name: String, 18 | @ColumnInfo(name = "date") var createdDate: Long, 19 | @ColumnInfo(name = "color") var color: Int) { 20 | 21 | @Ignore 22 | constructor(name: String, date: Long, color: Int) 23 | : this(UUID.randomUUID().toString(), name, date, color) 24 | 25 | companion object { 26 | /** Screenshots that has not yet been reviewed by the users */ 27 | const val UNCATEGORIZED = "uncategorized" 28 | 29 | /** Screenshots that had been reviewed by the users without categorizing it */ 30 | const val CATEGORY_NONE = "category_none" 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/FtsEntity.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.persistence 6 | 7 | import androidx.room.* 8 | 9 | @Fts4(contentEntity = ScreenshotContentModel::class) 10 | @Entity(tableName = "fts") 11 | data class FtsEntity( 12 | @ColumnInfo(name = "content_text") var contentText: String 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/LoadingViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.persistence 2 | 3 | data class LoadingViewModel (val primaryText: CharSequence? = null, val secondaryText: CharSequence? = null) -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/ScreenshotContentModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package org.mozilla.scryer.persistence 8 | 9 | import androidx.room.* 10 | import androidx.room.ForeignKey.CASCADE 11 | import java.util.* 12 | 13 | @Entity(tableName = "screenshot_content", 14 | foreignKeys = [(ForeignKey( 15 | entity = ScreenshotModel::class, 16 | parentColumns = ["id"], 17 | childColumns = ["id"], 18 | onDelete = CASCADE))]) 19 | data class ScreenshotContentModel constructor ( 20 | @PrimaryKey(autoGenerate = false) var id: String, 21 | @ColumnInfo(name = "content_text") var contentText: String? = null 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/ScreenshotDatabase.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.persistence 7 | 8 | import androidx.room.Database 9 | import androidx.room.RoomDatabase 10 | 11 | @Database( 12 | entities = [ 13 | CollectionModel::class, 14 | ScreenshotModel::class, 15 | ScreenshotContentModel::class, 16 | FtsEntity::class 17 | ], 18 | version = 2 19 | ) 20 | abstract class ScreenshotDatabase: RoomDatabase() { 21 | abstract fun screenshotDao(): ScreenshotDao 22 | abstract fun collectionDao(): CollectionDao 23 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/ScreenshotModel.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.persistence 7 | 8 | import androidx.room.* 9 | import org.mozilla.scryer.ScryerApplication 10 | import java.util.* 11 | 12 | @Entity(tableName = "screenshot", 13 | indices = [ 14 | Index("collection_id"), 15 | Index("absolute_path", unique = true) 16 | ] 17 | ) 18 | data class ScreenshotModel constructor ( 19 | @PrimaryKey(autoGenerate = false) var id: String, 20 | @ColumnInfo(name = "absolute_path") var absolutePath: String, 21 | @ColumnInfo(name = "last_modified") var lastModified: Long, 22 | @ColumnInfo(name = "collection_id") var collectionId: String 23 | ) { 24 | @Ignore 25 | constructor( 26 | absolutePath: String, 27 | lastModified: Long, 28 | collectionId: String 29 | ) : this(UUID.randomUUID().toString(), absolutePath, lastModified, collectionId) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/persistence/SuggestCollectionHelper.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.persistence 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | 6 | class SuggestCollectionHelper { 7 | companion object { 8 | val SUGGEST_COLOR = Color.parseColor("#00C8D7") 9 | val suggestCollections = listOf( 10 | CollectionModel("default1", "", Long.MAX_VALUE - 5, SUGGEST_COLOR), 11 | CollectionModel("default2", "", Long.MAX_VALUE - 4, SUGGEST_COLOR), 12 | CollectionModel("default3", "", Long.MAX_VALUE - 3, SUGGEST_COLOR), 13 | CollectionModel("default4", "", Long.MAX_VALUE - 2, SUGGEST_COLOR), 14 | CollectionModel("default5", "", Long.MAX_VALUE - 1, SUGGEST_COLOR)) 15 | 16 | fun isSuggestCollection(collection: CollectionModel): Boolean { 17 | return suggestCollections.any { it.id == collection.id } 18 | } 19 | 20 | fun getSuggestCollectionNameForTelemetry(context: Context?, name: String?): String { 21 | return name ?: "" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/promote/PromoteDialogHelper.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.promote 6 | 7 | import android.content.Context 8 | import android.graphics.drawable.Drawable 9 | import androidx.appcompat.app.AlertDialog 10 | import android.view.View 11 | import android.widget.ImageView 12 | import kotlinx.android.synthetic.main.dialog_promote.view.* 13 | import org.mozilla.scryer.R 14 | 15 | class PromoteDialogHelper { 16 | companion object { 17 | fun createPromoteDialog( 18 | context: Context, 19 | title: String, 20 | subtitle: String, 21 | drawable: Drawable?, 22 | positiveText: String, 23 | positiveListener: () -> Unit, 24 | negativeText: String, 25 | negativeListener: () -> Unit 26 | ): AlertDialog { 27 | val dialog = AlertDialog.Builder(context).create() 28 | val dialogView = View.inflate(context, R.layout.dialog_promote, null).let { 29 | it.title.text = title 30 | it.subtitle.text = subtitle 31 | drawable?.let { image -> 32 | it.findViewById(R.id.image).setImageDrawable(image) 33 | } 34 | 35 | it.positive_button.text = positiveText 36 | it.positive_button.setOnClickListener { _ -> 37 | dialog.dismiss() 38 | positiveListener.invoke() 39 | } 40 | 41 | it.negative_button.text = negativeText 42 | it.negative_button.setOnClickListener { _ -> 43 | dialog.dismiss() 44 | negativeListener.invoke() 45 | } 46 | it 47 | } 48 | dialog.setView(dialogView) 49 | return dialog 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/promote/Promoter.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.promote 6 | 7 | import android.content.Context 8 | import android.content.SharedPreferences 9 | import androidx.preference.PreferenceManager 10 | 11 | class Promoter { 12 | companion object { 13 | 14 | const val KEY_TAKE_SCREENSHOT = "promote_cond_take_screenshot" 15 | const val KEY_SORT_SCREENSHOT = "promote_cond_sort_screenshot" 16 | const val KEY_TAP_OCR = "promote_cond_tap_ocr_button" 17 | 18 | fun onScreenshotTaken(context: Context) { 19 | incPref(context, KEY_TAKE_SCREENSHOT) 20 | } 21 | 22 | fun onScreenshotSorted(context: Context) { 23 | incPref(context, KEY_SORT_SCREENSHOT) 24 | } 25 | 26 | fun onOcrButtonClicked(context: Context) { 27 | incPref(context, KEY_TAP_OCR) 28 | } 29 | 30 | private fun getPref(context: Context): SharedPreferences { 31 | return PreferenceManager.getDefaultSharedPreferences(context) 32 | } 33 | 34 | private fun incPref(context: Context, key: String) { 35 | val pref = getPref(context) 36 | val old = pref.getInt(key, 0) 37 | pref.edit().putInt(key, old + 1).apply() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/repository/ScreenshotRepository.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.repository 7 | 8 | import androidx.lifecycle.LiveData 9 | import android.content.Context 10 | import org.mozilla.scryer.persistence.CollectionModel 11 | import org.mozilla.scryer.persistence.ScreenshotContentModel 12 | import org.mozilla.scryer.persistence.ScreenshotModel 13 | 14 | interface ScreenshotRepository { 15 | companion object Factory { 16 | fun createRepository(context: Context, onCreated: () -> Unit): ScreenshotRepository { 17 | return ScreenshotDatabaseRepository.create(context, onCreated) 18 | } 19 | } 20 | 21 | fun addCollection(collection: CollectionModel) 22 | fun getCollections(): LiveData> 23 | fun getCollectionList(): List 24 | fun getCollection(id: String): CollectionModel? 25 | /** collection_id to model */ 26 | fun getCollectionCovers(): LiveData> 27 | fun updateCollection(collection: CollectionModel) 28 | fun updateCollectionId(collection: CollectionModel, id: String) 29 | fun deleteCollection(collection: CollectionModel) 30 | 31 | fun addScreenshot(screenshots: List) 32 | fun updateScreenshots(screenshots: List) 33 | fun getScreenshot(screenshotId: String): ScreenshotModel? 34 | fun getScreenshots(): LiveData> 35 | fun getScreenshotList(): List 36 | fun getScreenshots(collectionIds: List): LiveData> 37 | fun getScreenshotList(collectionIds: List): List 38 | fun deleteScreenshot(screenshot: ScreenshotModel) 39 | fun searchScreenshots(queryText: String): LiveData> 40 | fun searchScreenshotList(queryText: String): List 41 | 42 | fun getScreenshotContent(): LiveData> 43 | fun updateScreenshotContent(screenshotContent: ScreenshotContentModel) 44 | fun getContentText(screenshot: ScreenshotModel): String? 45 | 46 | fun setupDefaultContent(context: Context) { 47 | TODO("not implemented") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/scan/BackgroundScanner.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.scan 6 | 7 | import android.content.Context 8 | import androidx.concurrent.futures.ResolvableFuture 9 | import androidx.work.ListenableWorker 10 | import androidx.work.WorkerParameters 11 | import com.google.common.util.concurrent.ListenableFuture 12 | import kotlinx.coroutines.experimental.* 13 | import kotlin.coroutines.experimental.CoroutineContext 14 | 15 | class BackgroundScanner( 16 | context: Context, 17 | params: WorkerParameters 18 | ) : ListenableWorker(context, params), CoroutineScope { 19 | 20 | private val workerJob = Job() 21 | 22 | override val coroutineContext: CoroutineContext 23 | get() = Dispatchers.Default + workerJob + CoroutineExceptionHandler { _, _ -> } 24 | 25 | override fun startWork(): ListenableFuture { 26 | val future = ResolvableFuture.create() 27 | 28 | launch { 29 | FirebaseVisionTextHelper.scanAndSave() 30 | }.invokeOnCompletion { 31 | future.set(Result.success()) 32 | workerJob.cancel() 33 | } 34 | 35 | return future 36 | } 37 | 38 | override fun onStopped() { 39 | workerJob.cancel() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/scan/ContentScanner.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.scan 6 | 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.Transformations 9 | 10 | class ContentScanner { 11 | companion object { 12 | internal const val TAG = "ContentScanner" 13 | } 14 | 15 | private lateinit var plan: Plan 16 | 17 | fun onCreate(plan: Plan) { 18 | this.plan = plan 19 | plan.onCreate() 20 | } 21 | 22 | fun onDestroy() { 23 | plan.onDestroy() 24 | } 25 | 26 | fun getProgress(): LiveData> { 27 | return Transformations.map(plan.getProgressState()) { 28 | if (it is ProgressState.Progress) { 29 | Pair(it.current, it.total) 30 | } else { 31 | Pair(0, 0) 32 | } 33 | } 34 | } 35 | 36 | fun getProgressState(): LiveData { 37 | return plan.getProgressState() 38 | } 39 | 40 | fun isScanning(): Boolean { 41 | return plan.isScanning() 42 | } 43 | 44 | interface Plan { 45 | fun onCreate() 46 | fun onDestroy() 47 | fun getProgressState(): LiveData 48 | fun isScanning(): Boolean 49 | } 50 | 51 | sealed class ProgressState { 52 | object Unavailable: ProgressState() 53 | class Progress(var current: Int, var total: Int): ProgressState() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/setting/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.setting 7 | 8 | import android.os.Bundle 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.appcompat.widget.Toolbar 11 | import android.view.View 12 | import org.mozilla.scryer.R 13 | 14 | class SettingsActivity : AppCompatActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_settings) 18 | 19 | val toolbar = findViewById(R.id.toolbar) as Toolbar 20 | setSupportActionBar(toolbar) 21 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 22 | toolbar.setNavigationOnClickListener { finish() } 23 | 24 | if (savedInstanceState == null) { 25 | supportFragmentManager.beginTransaction() 26 | .replace(R.id.container, SettingsFragment()) 27 | .commitAllowingStateLoss() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/setting/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.setting 7 | 8 | import androidx.lifecycle.LiveData 9 | 10 | interface SettingsRepository { 11 | var serviceEnabled: Boolean 12 | val serviceEnabledObserver: LiveData 13 | 14 | var floatingEnable: Boolean 15 | val floatingEnableObservable: LiveData 16 | 17 | var addToCollectionEnable: Boolean 18 | val addToCollectionEnableObservable: LiveData 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/setting/TelemetrySwitchPreference.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.setting 2 | 3 | 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.preference.PreferenceViewHolder 8 | import androidx.preference.SwitchPreferenceCompat 9 | import android.util.AttributeSet 10 | import android.widget.TextView 11 | import org.mozilla.scryer.R 12 | import java.util.* 13 | 14 | class TelemetrySwitchPreference : SwitchPreferenceCompat { 15 | 16 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 17 | 18 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 19 | 20 | override fun onBindViewHolder(holder: PreferenceViewHolder) { 21 | val learnMore = holder.itemView.findViewById(R.id.learnMore) 22 | learnMore.setOnClickListener { v -> 23 | // This is a hardcoded link: if we ever end up needing more of these links, we should 24 | // move the link into an xml parameter, but there's no advantage to making it configurable now. 25 | val url = getTelemetryDataUsageUrl(context) 26 | 27 | val intent = Intent() 28 | intent.action = Intent.ACTION_VIEW 29 | intent.data = Uri.parse(url) 30 | context.startActivity(intent) 31 | } 32 | 33 | super.onBindViewHolder(holder) 34 | } 35 | 36 | private fun getTelemetryDataUsageUrl(context: Context): String { 37 | return context.getString(R.string.telemetry_data_usage_url, 38 | getAppVersion(context), 39 | getLanguageTag(Locale.getDefault())) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/setting/YourRightsFragment.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.setting 2 | 3 | import android.os.Bundle 4 | import android.text.Html 5 | import android.text.method.LinkMovementMethod 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.appcompat.app.ActionBar 11 | import androidx.fragment.app.Fragment 12 | import org.mozilla.scryer.R 13 | import org.mozilla.scryer.getSupportActionBar 14 | 15 | class YourRightsFragment : Fragment() { 16 | companion object { 17 | const val TAG: String = "YourRightsFragment" 18 | } 19 | 20 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 21 | val view: View = inflater.inflate(R.layout.fragment_your_rights, container, false) 22 | 23 | val content = StringBuilder() 24 | content.apply { 25 | append("

").append(getString(R.string.your_rights_content_1, getString(R.string.app_full_name))).append("

") 26 | append("

").append(getString(R.string.your_rights_content_2, getString(R.string.app_full_name))).append("

") 27 | append("

").append(getString(R.string.your_rights_content_3, getString(R.string.about_privacy_notice_url))).append("

") 28 | append("

").append(getString(R.string.your_rights_content_4, getString(R.string.app_full_name))).append("

") 29 | } 30 | 31 | val yourRightsTextView = view.findViewById(R.id.your_rights_content) 32 | yourRightsTextView?.text = Html.fromHtml(content.toString()) 33 | yourRightsTextView?.movementMethod = LinkMovementMethod.getInstance() 34 | 35 | return view 36 | } 37 | 38 | override fun onActivityCreated(savedInstanceState: Bundle?) { 39 | super.onActivityCreated(savedInstanceState) 40 | setupActionBar() 41 | } 42 | 43 | private fun setupActionBar() { 44 | getSupportActionBar(activity).apply { 45 | setDisplayHomeAsUpEnabled(true) 46 | updateActionBarTitle(this) 47 | } 48 | } 49 | 50 | private fun updateActionBarTitle(actionBar: ActionBar) { 51 | actionBar.title = getString(R.string.about_list_right) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/telemetry/CaptureServiceHeartbeatWorker.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.telemetry 2 | 3 | import android.app.ActivityManager 4 | import android.content.Context 5 | import androidx.work.Worker 6 | import androidx.work.WorkerParameters 7 | import org.mozilla.scryer.ScryerService 8 | 9 | 10 | class CaptureServiceHeartbeatWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { 11 | 12 | companion object { 13 | const val TAG = "CaptureServiceHeartbeatWorker" 14 | } 15 | 16 | override fun doWork(): Result { 17 | 18 | if (isCaptureServiceRunning()) { 19 | TelemetryWrapper.logActiveBackgroundService() 20 | } 21 | return Result.success() 22 | } 23 | 24 | private fun isCaptureServiceRunning(): Boolean { 25 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 26 | for (service in manager.getRunningServices(Integer.MAX_VALUE)) { 27 | if (ScryerService::class.java.name == service.service.className) { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/BottomDialog.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.ui 2 | 3 | import android.content.Context 4 | import androidx.annotation.LayoutRes 5 | import com.google.android.material.bottomsheet.BottomSheetBehavior 6 | import com.google.android.material.bottomsheet.BottomSheetDialog 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.ViewTreeObserver 10 | import org.mozilla.scryer.R 11 | 12 | class BottomDialogFactory { 13 | companion object { 14 | fun create(context: Context, @LayoutRes layoutId: Int): BottomSheetDialog { 15 | val dialog = BottomSheetDialog(context, R.style.ScryerBottomSheetDialogTheme) 16 | val view = View.inflate(context, layoutId, null) 17 | dialog.setContentView(view, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 18 | ViewGroup.LayoutParams.WRAP_CONTENT)) 19 | view.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { 20 | override fun onPreDraw(): Boolean { 21 | view.viewTreeObserver.removeOnPreDrawListener(this) 22 | BottomSheetBehavior.from(view.parent as ViewGroup).peekHeight = view.measuredHeight 23 | return false 24 | } 25 | }) 26 | return dialog 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/ConfirmationDialog.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.ui 7 | 8 | import android.content.Context 9 | import android.content.DialogInterface 10 | import androidx.appcompat.app.AlertDialog 11 | import android.view.View 12 | import android.widget.TextView 13 | import org.mozilla.scryer.R 14 | 15 | class ConfirmationDialog private constructor( 16 | private val dialog: AlertDialog, 17 | val viewHolder: ViewHolder 18 | ) { 19 | companion object { 20 | fun build( 21 | context: Context, 22 | title: String, 23 | positiveButtonText: String, 24 | positiveButtonListener: DialogInterface.OnClickListener, 25 | negativeButtonText: String, 26 | negativeButtonListener: DialogInterface.OnClickListener 27 | ): ConfirmationDialog { 28 | val view = View.inflate(context, R.layout.dialog_confirmation, null) 29 | val dialog = AlertDialog.Builder(context) 30 | .setView(view) 31 | .setTitle(title) 32 | .setPositiveButton(positiveButtonText, positiveButtonListener) 33 | .setNegativeButton(negativeButtonText, negativeButtonListener) 34 | .create() 35 | val holder = ViewHolder() 36 | holder.message = view.findViewById(R.id.confirmation_message) 37 | holder.subMessage = view.findViewById(R.id.confirmation_message_content_first_line) 38 | holder.subMessage2 = view.findViewById(R.id.confirmation_message_content_second_line) 39 | return ConfirmationDialog(dialog, holder) 40 | } 41 | } 42 | 43 | fun asAlertDialog(): AlertDialog { 44 | return dialog 45 | } 46 | 47 | class ViewHolder { 48 | var message: TextView? = null 49 | var subMessage: TextView? = null 50 | var subMessage2: TextView? = null 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/GridItemDecoration.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.ui 7 | 8 | import android.graphics.Rect 9 | import android.view.View 10 | import androidx.recyclerview.widget.RecyclerView 11 | 12 | open class GridItemDecoration(private val columnCount: Int, 13 | private val left: Int, 14 | private val top: Int, 15 | private val right: Int, 16 | private val vSpace: Int, 17 | private val hSpace: Int) : RecyclerView.ItemDecoration() { 18 | 19 | constructor(columnCount: Int, space: Int) : this(columnCount, space, space, space, space, space) 20 | 21 | override fun getItemOffsets(outRect: Rect, view: View, 22 | parent: RecyclerView, state: RecyclerView.State) { 23 | val position = parent.getChildViewHolder(view).adapterPosition 24 | if (position < 0) { 25 | return 26 | } 27 | 28 | setSpaces(outRect, position) 29 | } 30 | 31 | open fun setSpaces(outRect: Rect, position: Int) { 32 | outRect.left = if (position % columnCount == 0) left else hSpace / 2 33 | outRect.top = if (position < columnCount) top else 0 34 | outRect.right = if (position % columnCount == columnCount - 1) right else hSpace / 2 35 | outRect.bottom = vSpace 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/InnerSpaceDecoration.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.ui 7 | 8 | import android.graphics.Rect 9 | import android.view.View 10 | 11 | class InnerSpaceDecoration(private val space: Int, 12 | private val columnCountProvider: () -> Int) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() { 13 | override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView, state: androidx.recyclerview.widget.RecyclerView.State) { 14 | /** 15 | * x: padding for left/right-most items (left-most item only have right padding, vice versa) 16 | * y: padding for middle items (padding at both left & right sides) 17 | * 18 | * size of left/right-most items must be equal to the size of middle items 19 | * x = 2y 20 | * 21 | * 2x + 2(middleItemCount)y = sum of all padding 22 | * => 2x + 2(itemCount - 2)y = space * (itemCount - 1) 23 | * => 4y + 2(itemCount - 2)y = space * (itemCount - 1) 24 | * => y = space * (itemCount - 1) / (4 + 2 * (itemCount - 2))y 25 | */ 26 | val position = parent.getChildAdapterPosition(view) 27 | val columnCount = columnCountProvider.invoke() 28 | val y = space * (columnCount - 1) / (4 + 2 * (columnCount - 2)) 29 | val x = y * 2 30 | 31 | outRect.top = 0 32 | outRect.bottom = space 33 | when { 34 | position % columnCount == 0 -> { 35 | outRect.left = 0 36 | outRect.right = x 37 | } 38 | position % columnCount == columnCount - 1 -> { 39 | outRect.left = x 40 | outRect.right = 0 41 | } 42 | else -> { 43 | outRect.left = y 44 | outRect.right = y 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/LockableViewPager.kt: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package org.mozilla.scryer.ui 6 | 7 | import android.annotation.SuppressLint 8 | import android.content.Context 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import androidx.viewpager.widget.ViewPager 12 | 13 | class LockableViewPager : ViewPager { 14 | var pageLocked = false 15 | 16 | constructor(context: Context) : super(context) 17 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 18 | 19 | @SuppressLint("ClickableViewAccessibility") 20 | override fun onTouchEvent(ev: MotionEvent?): Boolean { 21 | if (pageLocked) { 22 | return false 23 | } 24 | return super.onTouchEvent(ev) 25 | } 26 | 27 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 28 | if (pageLocked) { 29 | return false 30 | } 31 | return super.onInterceptTouchEvent(ev) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/ScryerSnackbar.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.ui 2 | 3 | import android.graphics.Color 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.core.content.ContextCompat 7 | import androidx.core.graphics.drawable.DrawableCompat 8 | import com.google.android.material.snackbar.Snackbar 9 | import org.mozilla.scryer.R 10 | 11 | class ScryerSnackbar { 12 | companion object { 13 | 14 | fun make(view: View, text: String, duration: Int): Snackbar { 15 | val bar = Snackbar.make(view, text, duration) 16 | 17 | ContextCompat.getDrawable(view.context, R.drawable.rect_4dp)?.apply { 18 | val wrapped = DrawableCompat.wrap(this) 19 | DrawableCompat.setTint(wrapped, ContextCompat.getColor(view.context, R.color.primaryTeal)) 20 | bar.view.background = wrapped 21 | 22 | val params = bar.view.layoutParams 23 | // TODO: Visual spec for margin 24 | val margin = view.resources.getDimensionPixelSize(R.dimen.toast_horizontal_margin) 25 | if (params is ViewGroup.MarginLayoutParams) { 26 | params.marginStart = margin 27 | params.marginEnd = margin 28 | params.bottomMargin = margin 29 | } 30 | bar.view.layoutParams = params 31 | bar.setActionTextColor(Color.WHITE) 32 | } 33 | 34 | return bar 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/ui/ScryerToast.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.ui 2 | 3 | import android.content.Context 4 | import android.view.Gravity 5 | import android.view.View 6 | import android.widget.TextView 7 | import android.widget.Toast 8 | import org.mozilla.scryer.R 9 | 10 | class ScryerToast(private val context: Context) { 11 | companion object { 12 | 13 | /** 14 | * Use ScryerToast#show() if you're likely to show toast multiple times within the same page, 15 | * so the toast view can be reused instead of inflating a new one each time this method is called. 16 | */ 17 | fun makeText(context: Context, text: String, duration: Int): Toast { 18 | val toast = Toast(context) 19 | toast.setGravity(Gravity.FILL_HORIZONTAL or Gravity.BOTTOM, 0, 0) 20 | toast.view = View.inflate(context, R.layout.view_custom_toast, null) 21 | toast.view.findViewById(R.id.text)?.text = text 22 | toast.duration = duration 23 | return toast 24 | } 25 | } 26 | 27 | private var toast: Toast? = null 28 | private var rootView: View = View.inflate(context, R.layout.view_custom_toast, null) 29 | private val textView: TextView by lazy { 30 | rootView.findViewById(R.id.text) 31 | } 32 | 33 | fun show(msg: String, toastDuration: Int, yOffset: Int = 0) { 34 | textView.text = msg 35 | toast?.cancel() 36 | 37 | toast = Toast(context).apply { 38 | setGravity(Gravity.FILL_HORIZONTAL or Gravity.BOTTOM, 0, yOffset) 39 | view = rootView 40 | duration = toastDuration 41 | show() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/util/ThreadUtils.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.util 7 | 8 | import android.os.Looper 9 | import kotlinx.coroutines.experimental.* 10 | import java.util.concurrent.Executors 11 | import java.util.concurrent.ThreadFactory 12 | import java.util.concurrent.atomic.AtomicInteger 13 | 14 | object ThreadUtils { 15 | private val backgroundExecutorService = Executors.newSingleThreadExecutor(ioPrioritisedFactory) 16 | private val uiThread = Looper.getMainLooper().thread 17 | 18 | private val ioPrioritisedFactory: ThreadFactory 19 | get() = CustomThreadFactory("pool-io-background", Thread.NORM_PRIORITY - 1) 20 | 21 | val singleThreadDispatcher = backgroundExecutorService.asCoroutineDispatcher() 22 | 23 | fun assertOnUiThread() { 24 | val currentThread = Thread.currentThread() 25 | val currentThreadId = currentThread.id 26 | val expectedThreadId = uiThread.id 27 | 28 | if (currentThreadId == expectedThreadId) { 29 | return 30 | } 31 | 32 | throw IllegalThreadStateException("Expected UI thread, but running on " + currentThread.name) 33 | } 34 | 35 | private class CustomThreadFactory(private val threadName: String, private val threadPriority: Int) : ThreadFactory { 36 | private val mNumber = AtomicInteger() 37 | 38 | override fun newThread(r: Runnable): Thread { 39 | val thread = Thread(r, threadName + "-" + mNumber.getAndIncrement()) 40 | thread.priority = threadPriority 41 | return thread 42 | } 43 | } 44 | } 45 | 46 | fun launchIO(block: suspend CoroutineScope.() -> Unit) { 47 | GlobalScope.launch(Dispatchers.IO) { 48 | block(this) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/util/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mozilla.scryer.util 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | 7 | fun showKeyboard(view: View) { 8 | val imm = view.context 9 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 10 | imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) 11 | } 12 | 13 | fun hideKeyboard(view: View) { 14 | val imm = view.context 15 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 16 | imm.hideSoftInputFromWindow(view.windowToken, 0) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/mozilla/scryer/viewmodel/ScreenshotViewModel.kt: -------------------------------------------------------------------------------- 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | package org.mozilla.scryer.viewmodel 7 | 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.FragmentActivity 10 | import androidx.lifecycle.ViewModel 11 | import androidx.lifecycle.ViewModelProvider 12 | import androidx.lifecycle.ViewModelProviders 13 | import kotlinx.coroutines.experimental.Dispatchers 14 | import kotlinx.coroutines.experimental.withContext 15 | import org.mozilla.scryer.ScryerApplication 16 | import org.mozilla.scryer.persistence.ScreenshotModel 17 | import org.mozilla.scryer.repository.ScreenshotRepository 18 | 19 | class ScreenshotViewModel(private val delegate: ScreenshotRepository) : ViewModel(), 20 | ScreenshotRepository by delegate { 21 | companion object { 22 | fun get(fragment: Fragment): ScreenshotViewModel { 23 | return ViewModelProviders.of(fragment, getFactory()).get(ScreenshotViewModel::class.java) 24 | } 25 | 26 | fun get(activity: FragmentActivity): ScreenshotViewModel { 27 | return ViewModelProviders.of(activity, getFactory()).get(ScreenshotViewModel::class.java) 28 | } 29 | 30 | private fun getFactory(): ScreenshotViewModelFactory { 31 | return ScreenshotViewModelFactory(ScryerApplication.getScreenshotRepository()) 32 | } 33 | } 34 | 35 | suspend fun batchMove( 36 | screenshots: List, 37 | collectionId: String 38 | ) = withContext(Dispatchers.Default) { 39 | screenshots.forEach { 40 | it.collectionId = collectionId 41 | updateScreenshots(listOf(it)) 42 | } 43 | } 44 | } 45 | 46 | class ScreenshotViewModelFactory(private val repository: ScreenshotRepository) 47 | : ViewModelProvider.NewInstanceFactory() { 48 | override fun create(modelClass: Class): T { 49 | @Suppress("UNCHECKED_CAST") 50 | return ScreenshotViewModel(repository) as T 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/slid_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/primary_text_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/image_logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-hdpi/image_logotype.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/image_logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-mdpi/image_logotype.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/image_logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xhdpi/image_logotype.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/ic_stat_notify.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_appbeta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_appbeta.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_emptypage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_emptypage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_error.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_feedback.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_logotype.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_noresult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_noresult.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_searchhint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_searchhint.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/image_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/image_welcome.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/img_ocrhint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/img_ocrhint.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/img_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla-tw/ScreenshotGo/c827ba8a40e0897d0dd3513c389ed422688e183f/app/src/main/res/drawable-xxhdpi/img_pointer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_dialog_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/capture.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/capture_button_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/capture_button_exit_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/capture_button_exit_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/check.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_2dp_grey50.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_2dp_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/close_large.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/close_small.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/collection_name_dialog_cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/contained_button_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/debug_broken_svg.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/detailpage_toolbar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fab_ocr.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hint.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_new_collection_item_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_quick_access_empty_view_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stat_notify_sort.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/more.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/move.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outlined_button_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rect_2dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rect_3dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rect_4dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/screenshot_select_checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selected.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sorted_collection_item_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sorting_panel_create_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sorting_panel_hint_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sorting_panel_item_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sorting_panel_suggest_item_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/unsorted_collection_item_bkg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/viewmore.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/websearch.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 16 | 23 | 24 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_sorting_panel.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 16 | 17 | 24 | 25 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_svg_viewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_confirmation.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 27 | 28 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_give_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 32 | 33 | 36 | 37 | 42 | 43 | 44 | 49 | 50 |