Repository: liato/android-bankdroid Branch: master Commit: 3a164ae699b2 Files: 328 Total size: 60.6 MB Directory structure: gitextract_2xrfz_mg/ ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE ├── README.rst ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── crashlytics.properties.example │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── aidl/ │ │ │ └── com/ │ │ │ └── sonyericsson/ │ │ │ └── extras/ │ │ │ └── liveview/ │ │ │ ├── IPluginServiceCallbackV1.aidl │ │ │ └── IPluginServiceV1.aidl │ │ ├── java/ │ │ │ ├── com/ │ │ │ │ ├── ast/ │ │ │ │ │ └── util/ │ │ │ │ │ └── CookieParser.java │ │ │ │ └── liato/ │ │ │ │ └── bankdroid/ │ │ │ │ ├── AboutActivity.java │ │ │ │ ├── ActivityHelper.java │ │ │ │ ├── BankEditActivity.java │ │ │ │ ├── BankdroidApplication.java │ │ │ │ ├── BetterPopupWindow.java │ │ │ │ ├── DataRetrieverTask.java │ │ │ │ ├── LockableActivity.java │ │ │ │ ├── LockablePreferenceActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── PairApplicationsActivity.java │ │ │ │ ├── SettingsActivity.java │ │ │ │ ├── StartupReceiver.java │ │ │ │ ├── TimePreference.java │ │ │ │ ├── TransactionsActivity.java │ │ │ │ ├── WebViewActivity.java │ │ │ │ ├── adapters/ │ │ │ │ │ └── AccountsAdapter.java │ │ │ │ ├── appwidget/ │ │ │ │ │ ├── AutoRefreshService.java │ │ │ │ │ ├── BankdroidWidgetProvider.java │ │ │ │ │ ├── BankdroidWidgetProvider_2x1.java │ │ │ │ │ ├── BankdroidWidgetProvider_4x1.java │ │ │ │ │ └── WidgetConfigureActivity.java │ │ │ │ ├── banking/ │ │ │ │ │ └── BankFactory.java │ │ │ │ ├── db/ │ │ │ │ │ ├── DBAdapter.java │ │ │ │ │ ├── Database.java │ │ │ │ │ ├── DatabaseHelper.java │ │ │ │ │ └── LegacyDatabase.java │ │ │ │ ├── liveview/ │ │ │ │ │ ├── LiveViewService.java │ │ │ │ │ ├── PluginConstants.java │ │ │ │ │ ├── PluginReceiver.java │ │ │ │ │ └── PluginUtils.java │ │ │ │ ├── lockpattern/ │ │ │ │ │ ├── ChooseLockPattern.java │ │ │ │ │ ├── ChooseLockPatternExample.java │ │ │ │ │ ├── ChooseLockPatternTutorial.java │ │ │ │ │ ├── ConfirmLockPattern.java │ │ │ │ │ ├── LinearLayoutWithDefaultTouchRecepient.java │ │ │ │ │ ├── LockPatternUtils.java │ │ │ │ │ └── LockPatternView.java │ │ │ │ ├── provider/ │ │ │ │ │ └── BankTransactionsProvider.java │ │ │ │ └── utils/ │ │ │ │ ├── EmulatorUtils.java │ │ │ │ ├── LoggingUtils.java │ │ │ │ └── NetworkUtils.java │ │ │ └── net/ │ │ │ ├── margaritov/ │ │ │ │ └── preference/ │ │ │ │ └── colorpicker/ │ │ │ │ ├── AlphaPatternDrawable.java │ │ │ │ ├── ColorPickerDialog.java │ │ │ │ ├── ColorPickerPanelView.java │ │ │ │ ├── ColorPickerPreference.java │ │ │ │ └── ColorPickerView.java │ │ │ └── sf/ │ │ │ └── andhsli/ │ │ │ └── hotspotlogin/ │ │ │ └── SimpleCrypto.java │ │ └── res/ │ │ ├── anim/ │ │ │ ├── grow_from_bottom.xml │ │ │ ├── grow_from_top.xml │ │ │ ├── grow_from_topleft_to_bottomright.xml │ │ │ ├── shrink_from_bottom.xml │ │ │ ├── shrink_from_bottomright_to_topleft.xml │ │ │ ├── shrink_from_top.xml │ │ │ ├── zoom_enter.xml │ │ │ └── zoom_exit.xml │ │ ├── drawable/ │ │ │ ├── btn_check.xml │ │ │ ├── lock_anim.xml │ │ │ ├── menu_button.xml │ │ │ ├── popup_button.xml │ │ │ └── widget_progress.xml │ │ ├── layout/ │ │ │ ├── about.xml │ │ │ ├── bank.xml │ │ │ ├── bank_spinner_dropdown_item.xml │ │ │ ├── bank_spinner_item.xml │ │ │ ├── choose_lock_pattern.xml │ │ │ ├── choose_lock_pattern_example.xml │ │ │ ├── choose_lock_pattern_tutorial.xml │ │ │ ├── confirm_lock_pattern.xml │ │ │ ├── dialog_color_picker.xml │ │ │ ├── empty.xml │ │ │ ├── listitem_accounts_group.xml │ │ │ ├── listitem_accounts_item.xml │ │ │ ├── main.xml │ │ │ ├── pair_applications_layout.xml │ │ │ ├── popup_account.xml │ │ │ ├── popup_bank.xml │ │ │ ├── toolbar.xml │ │ │ ├── transaction_date.xml │ │ │ ├── transaction_item.xml │ │ │ ├── transactions.xml │ │ │ ├── webview.xml │ │ │ ├── widget.xml │ │ │ ├── widget_large.xml │ │ │ ├── widget_large_transparent.xml │ │ │ └── widget_transparent.xml │ │ ├── layout-land/ │ │ │ ├── choose_lock_pattern.xml │ │ │ ├── confirm_lock_pattern.xml │ │ │ └── dialog_color_picker.xml │ │ ├── menu/ │ │ │ ├── about.xml │ │ │ └── main.xml │ │ ├── values/ │ │ │ ├── array.xml │ │ │ ├── colors.xml │ │ │ ├── config.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── themes.xml │ │ ├── values-sv/ │ │ │ ├── array.xml │ │ │ └── strings.xml │ │ ├── values-v21/ │ │ │ └── themes.xml │ │ └── xml/ │ │ ├── appwidget_info.xml │ │ ├── appwidget_info_large.xml │ │ └── settings.xml │ └── test/ │ └── java/ │ └── com/ │ └── liato/ │ └── bankdroid/ │ ├── DataRetrieverTaskTest.java │ └── appwidget/ │ └── DataRetrieverTaskTest.java ├── assets/ │ ├── btn_check.psd │ ├── icabanken.psd │ ├── logos.psd │ └── widgets.psd ├── bankdroid-core/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── liato/ │ │ └── bankdroid/ │ │ └── configuration/ │ │ └── DefaultConnectionConfiguration.java │ └── resources/ │ └── i18n/ │ ├── application.properties │ └── application_sv_SE.properties ├── bankdroid-interface/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── liato/ │ └── bankdroid/ │ └── api/ │ ├── Provider.java │ ├── ProviderFactory.java │ ├── configuration/ │ │ ├── Entry.java │ │ ├── Field.java │ │ ├── FieldBuilder.java │ │ ├── FieldType.java │ │ ├── FieldValidator.java │ │ └── ProviderConfiguration.java │ ├── domain/ │ │ ├── ProviderConnection.java │ │ └── account/ │ │ ├── Account.java │ │ ├── CreditCardAccount.java │ │ ├── Equity.java │ │ ├── EquityAccount.java │ │ ├── LiabilityAccount.java │ │ ├── Payment.java │ │ ├── PrePaidCardAccount.java │ │ ├── Transaction.java │ │ ├── TransactionAccount.java │ │ └── impl/ │ │ ├── AbstractAccountBuilder.java │ │ ├── AccountBuilder.java │ │ ├── CreditCardAccountBuilder.java │ │ ├── EquityAccountBuilder.java │ │ ├── EquityBuilder.java │ │ ├── LiabilityAccountBuilder.java │ │ ├── PrePaidCardAccountBuilder.java │ │ ├── TransactionAccountBuilder.java │ │ └── TransactionBuilder.java │ └── service/ │ └── ServiceLoader.java ├── bankdroid-legacy/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ ├── com/ │ │ │ │ └── liato/ │ │ │ │ └── bankdroid/ │ │ │ │ ├── Helpers.java │ │ │ │ ├── banking/ │ │ │ │ │ ├── Account.java │ │ │ │ │ ├── Bank.java │ │ │ │ │ ├── BankChoice.java │ │ │ │ │ ├── BasicProviderConfiguration.java │ │ │ │ │ ├── LegacyBankFactory.java │ │ │ │ │ ├── LegacyBankHelper.java │ │ │ │ │ ├── LegacyProviderConfiguration.java │ │ │ │ │ ├── Transaction.java │ │ │ │ │ ├── banks/ │ │ │ │ │ │ ├── AbsIkanoPartner.java │ │ │ │ │ │ ├── AkeliusInvest.java │ │ │ │ │ │ ├── AkeliusSpar.java │ │ │ │ │ │ ├── AppeakPoker.java │ │ │ │ │ │ ├── BetterGlobe.java │ │ │ │ │ │ ├── Bioklubben.java │ │ │ │ │ │ ├── BlekingeTrafiken.java │ │ │ │ │ │ ├── Bredband2VoIP.java │ │ │ │ │ │ ├── BrummerKF.java │ │ │ │ │ │ ├── CSN.java │ │ │ │ │ │ ├── Chalmrest.java │ │ │ │ │ │ ├── DanskeBank.java │ │ │ │ │ │ ├── Everydaycard.java │ │ │ │ │ │ ├── FirstCard.java │ │ │ │ │ │ ├── Hemkop.java │ │ │ │ │ │ ├── Hors.java │ │ │ │ │ │ ├── IKEA.java │ │ │ │ │ │ ├── IkanoBank.java │ │ │ │ │ │ ├── Jojo.java │ │ │ │ │ │ ├── McDonalds.java │ │ │ │ │ │ ├── Meniga.java │ │ │ │ │ │ ├── MinPension.java │ │ │ │ │ │ ├── Nordnet.java │ │ │ │ │ │ ├── OKQ8.java │ │ │ │ │ │ ├── Ostgotatrafiken.java │ │ │ │ │ │ ├── Osuuspankki.java │ │ │ │ │ │ ├── Payson.java │ │ │ │ │ │ ├── PlusGirot.java │ │ │ │ │ │ ├── SevenDay.java │ │ │ │ │ │ ├── SveaDirekt.java │ │ │ │ │ │ ├── SvenskaSpel.java │ │ │ │ │ │ ├── TestBank.java │ │ │ │ │ │ ├── TicketRikskortet.java │ │ │ │ │ │ ├── Vasttrafik.java │ │ │ │ │ │ ├── Zidisha.java │ │ │ │ │ │ ├── americanexpress/ │ │ │ │ │ │ │ ├── AmericanExpress.java │ │ │ │ │ │ │ └── model/ │ │ │ │ │ │ │ ├── AccountActivity.java │ │ │ │ │ │ │ ├── Amount.java │ │ │ │ │ │ │ ├── Capabilities.java │ │ │ │ │ │ │ ├── Card.java │ │ │ │ │ │ │ ├── Date.java │ │ │ │ │ │ │ ├── LoginRequest.java │ │ │ │ │ │ │ ├── LoginResponse.java │ │ │ │ │ │ │ ├── LogonData.java │ │ │ │ │ │ │ ├── Summary.java │ │ │ │ │ │ │ ├── SummaryData.java │ │ │ │ │ │ │ ├── TotalBalance.java │ │ │ │ │ │ │ ├── Transaction.java │ │ │ │ │ │ │ ├── TransactionCapabilities.java │ │ │ │ │ │ │ ├── TransactionDetails.java │ │ │ │ │ │ │ └── TransactionsResponse.java │ │ │ │ │ │ ├── bitcoin/ │ │ │ │ │ │ │ ├── Bitcoin.java │ │ │ │ │ │ │ └── model/ │ │ │ │ │ │ │ ├── BlockchainResponse.java │ │ │ │ │ │ │ ├── Input.java │ │ │ │ │ │ │ ├── Out.java │ │ │ │ │ │ │ ├── PrevOut.java │ │ │ │ │ │ │ └── Transfer.java │ │ │ │ │ │ ├── coop/ │ │ │ │ │ │ │ ├── Coop.java │ │ │ │ │ │ │ └── model/ │ │ │ │ │ │ │ └── web/ │ │ │ │ │ │ │ ├── D.java │ │ │ │ │ │ │ ├── Model.java │ │ │ │ │ │ │ ├── Result.java │ │ │ │ │ │ │ └── WebTransactionHistoryResponse.java │ │ │ │ │ │ ├── ica/ │ │ │ │ │ │ │ ├── ICA.java │ │ │ │ │ │ │ └── model/ │ │ │ │ │ │ │ ├── Account.java │ │ │ │ │ │ │ ├── LoginError.java │ │ │ │ │ │ │ ├── Overview.java │ │ │ │ │ │ │ ├── Transaction.java │ │ │ │ │ │ │ └── User.java │ │ │ │ │ │ ├── lansforsakringar/ │ │ │ │ │ │ │ ├── Lansforsakringar.java │ │ │ │ │ │ │ └── model/ │ │ │ │ │ │ │ ├── request/ │ │ │ │ │ │ │ │ ├── AccountsRequest.java │ │ │ │ │ │ │ │ ├── ChallengeRequest.java │ │ │ │ │ │ │ │ ├── LoginRequest.java │ │ │ │ │ │ │ │ ├── TransactionsRequest.java │ │ │ │ │ │ │ │ └── UpcomingTransactionsRequest.java │ │ │ │ │ │ │ └── response/ │ │ │ │ │ │ │ ├── Account.java │ │ │ │ │ │ │ ├── AccountsResponse.java │ │ │ │ │ │ │ ├── ChallengeResponse.java │ │ │ │ │ │ │ ├── LoginResponse.java │ │ │ │ │ │ │ ├── NumberResponse.java │ │ │ │ │ │ │ ├── Transaction.java │ │ │ │ │ │ │ └── TransactionsResponse.java │ │ │ │ │ │ ├── nordea/ │ │ │ │ │ │ │ ├── CaptchaBreaker.java │ │ │ │ │ │ │ ├── CaptchaBreakerNumbers.java │ │ │ │ │ │ │ └── Nordea.java │ │ │ │ │ │ └── rikslunchen/ │ │ │ │ │ │ ├── Rikslunchen.java │ │ │ │ │ │ └── model/ │ │ │ │ │ │ └── Envelope.java │ │ │ │ │ └── exceptions/ │ │ │ │ │ ├── BankChoiceException.java │ │ │ │ │ ├── BankException.java │ │ │ │ │ └── LoginException.java │ │ │ │ ├── provider/ │ │ │ │ │ ├── IAccountTypes.java │ │ │ │ │ ├── IBankTransactionsProvider.java │ │ │ │ │ └── IBankTypes.java │ │ │ │ └── utils/ │ │ │ │ ├── ExceptionUtils.java │ │ │ │ ├── FieldTypeMapper.java │ │ │ │ ├── Installation.java │ │ │ │ └── StringUtils.java │ │ │ └── eu/ │ │ │ └── nullbyte/ │ │ │ └── android/ │ │ │ └── urllib/ │ │ │ ├── CertPinningSSLSocketFactory.java │ │ │ ├── CertPinningTrustManager.java │ │ │ ├── CertificateReader.java │ │ │ ├── ClientCertificate.java │ │ │ ├── HttpMethod.java │ │ │ └── Urllib.java │ │ └── res/ │ │ ├── raw/ │ │ │ ├── cert_akeliusinvest.pem │ │ │ ├── cert_akeliusspar.pem │ │ │ ├── cert_americanexpress_global.pem │ │ │ ├── cert_bioklubben.pem │ │ │ ├── cert_bredband2.pem │ │ │ ├── cert_brummer.pem │ │ │ ├── cert_coop.pem │ │ │ ├── cert_csn.pem │ │ │ ├── cert_danskebank.pem │ │ │ ├── cert_firstcard.pem │ │ │ ├── cert_hemkop.pem │ │ │ ├── cert_ica.pem │ │ │ ├── cert_ikanobank.pem │ │ │ ├── cert_ikanopartner.pem │ │ │ ├── cert_jojo.pem │ │ │ ├── cert_lansforsakringar.pem │ │ │ ├── cert_meniga.pem │ │ │ ├── cert_minpension.pem │ │ │ ├── cert_nordnet.pem │ │ │ ├── cert_okq8.pem │ │ │ ├── cert_ostgotatrafiken_login.pem │ │ │ ├── cert_ostgotatrafiken_overview.pem │ │ │ ├── cert_osuuspankki.pem │ │ │ ├── cert_osuuspankki_mobile.pem │ │ │ ├── cert_payson.pem │ │ │ ├── cert_plusgirot.pem │ │ │ ├── cert_rikslunchen.pem │ │ │ ├── cert_sevenday.pem │ │ │ ├── cert_sveadirekt.pem │ │ │ ├── cert_svenskaspel.pem │ │ │ ├── cert_ticketrikskortet.pem │ │ │ ├── cert_vasttrafik.pem │ │ │ ├── cert_zidisha.pem │ │ │ └── loading.html │ │ ├── values/ │ │ │ └── strings.xml │ │ └── values-sv/ │ │ └── strings.xml │ └── test/ │ └── java/ │ ├── com/ │ │ └── liato/ │ │ └── bankdroid/ │ │ └── utils/ │ │ └── ExceptionUtilsTest.java │ └── not/ │ └── bankdroid/ │ └── at/ │ └── all/ │ ├── ExceptionFactory.java │ └── ExceptionThrower.java ├── build.gradle ├── config/ │ ├── ide/ │ │ └── androidstudio/ │ │ └── AndroidStyle.xml │ └── quality/ │ ├── checkstyle/ │ │ ├── checkstyle.xml │ │ └── suppressions.xml │ ├── findbugs/ │ │ └── findbugs-filter.xml │ ├── lint/ │ │ └── lint.xml │ ├── pmd/ │ │ └── pmd-ruleset.xml │ └── quality.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools/ ├── nordea_captcha_breaker/ │ ├── captchabreaker.py │ ├── captchas_item_template.html │ └── captchas_template.html ├── refresh_bank_certificates └── update-suppressions.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ CHANGELOG merge=union ================================================ FILE: .gitignore ================================================ # built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ # Local configuration file (sdk path, etc) local.properties dev/ *.pyc .DS_Store *.orig # Local Eclipse files .classpath .project # Android studio .gradle build/ *.iws *.ipr *.swp out/ .idea/libraries .idea/workspace.xml .idea/misc.xml .idea/vcs.xml .idea/* *.iml .idea #Project specific app/src/main/java/com/liato/bankdroid/db/Crypto.* app/src/main/java/com/liato/bankdroid/db/Crypto.java.dev dev/ app/release.keystore app/crashlytics.properties app/keystore.properties ================================================ FILE: .travis.yml ================================================ language: android jdk: oraclejdk8 before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ env: matrix: - ANDROID_TARGET=android-25 ANDROID_ABI=armeabi-v7a android: components: - tools - platform-tools - build-tools-25.0.1 - android-25 - extra-android-m2repository script: ./gradlew assembleDebug check notifications: email: false slack: secure: asrV7/94tGqJbhotBAkPFi80PWJY3lcu7uYo855kpujXQHP61b0gC5gHnAairdD+MtrwICOmsJE9KIK/HOIpFrzwE+IwxJ+p6cGL9kX9blX+ZThcz1HkZgEK/EgaMSUxLKZFGrz0LUdktoZ9h+VixeRg05S4VijA7A814iA78fI= branches: except: - gh-pages - transaction-uid ================================================ FILE: CHANGELOG ================================================ Please view this file on the master branch, on stable branches it's out of date. (Unreleased) * Update certificates * Remove support for Avanza and Avanza Mini v1.9.14 (2017-01-06) * Updated certificates for First Card, Osuuspankki and Östgötatrafiken v1.9.13 (2016-11-03) * Fixes Crashlytics logging issue v1.9.12 (2016-11-02) * Bioklubben: Use https. It's secure and http page no longer exists. * Östgötatrafiken: Adapt to new login page (Facebook login not supported) * Removes broken encryption v1.9.11 (2016-10-26) * Warn about disabled banks in the transactions list * Show warning text about disabled banks in the main activity * Removes support for TrustBuddy since they're no longer in business * Removes support for Audi since they require MobiltBankId * Removes support for VolvoFinans since they require MobiltBankId * Removes support for EasyCard since they require MobiltBankId * Removes support for Preem since they require MobiltBankId * Removes support for ResursBank since they require MobiltBankId * Removes support for Seat since they require MobiltBankId * Removes support for Shell since they require MobiltBankId * Removes support for Skoda since they require MobiltBankId * Removes support for SupremeCard since they require MobiltBankId * Removes support for Villabanken since they require MobiltBankId * Removes support for Volkswagen since they require MobiltBankId * Merged NordnetDirekt with Nordnet. * Merged AvanzaMini with Avanza. v1.9.10.10 (2016-10-17) * Fixes crash for Amex * Strikethrough balance in widget indicates update problems. v1.9.10.9 (2016-10-12) * Länsförsäkringar: Remove broken web links * Improve Javadocs in the Bank class * Make successfully refreshing a disabled bank re-enable it * Östgötatrafiken: Update certificate * Östgötatrafiken: Adapt to changed web page * Skip Server Name Indication (SNI) when refreshing bank certificates to avoid certificate mismatches (bankdroid doesn't use SNI) * Ticket Rikskortet: Adapt to changed web page * Removes IcaBanken since they now require BankID * SveaDirekt: Adapt to changed web page * Rikslunchen: Adapt to new web service * AmericanExpress: Use their mobile API v1.9.10.8 (2016-09-29) * Add a Volatile account to the Test Bank that always changes its balance. * Fix widget not updating when the balance change was lower than the notification threshold. v1.9.10.7 (2016-08-18) * Update certificates for AmericanExpress and Coop v1.9.10.6 (2016-08-01) * Update outdated certificates * Upgrade to android-build-tools v.24 * Upgrade to gradle 2.14 v1.9.10.5 (2016-04-03) * Fix for OKQ8 * Update certificates for AmericanExpress, DankeBank, FirstCard, Rikslunchen and SveaDirekt. v1.9.10.4 (2016-02-25) * Removes support for EspressoHouse * Adds support Högskolerestaurangers kund-/studentkort * Update certificates for American Express, Marginalen, PayPal, Ticket Rikskortet, Zidisha, Östgötatrafiken v1.9.10.3 (2015-12-24) * Fix for Länsförsäkringar v1.9.10.2 (2015-12-22) * Fixes for Coop login. * Removes support for Forex Bank due to BankId requirement. * Fixes for MinPension login. * Update certificates for Coop, ICA, Osuuspankki, PlusGirot, SevenDay, Meniga & Steam. * Fixes for Bredband2. * Added support for Android 6.0 Marshmallow. v1.9.10.1 (2015-11-22) * Update certificate for ICA. * Fixes bug where banks without password could not be added. v1.9.10.0 (2015-11-06) * Removes support for PayPal due to complex web-login. * Removes support for Swedbank, Sparbankerna, Marginalen and NordeaDK due to BankId requirement. * Updates certificates for Bredband2, EasyCard, Nordea DK, Nordnet, Östgötatrafiken, Osuuspankki, PayPal, Villabanken * Adds support for custom properties for bank implementations. v1.9.9.5 (2015-07-18) * Updates certificates for Akelius Spar, Akelius Invest, Hemköp and Resurs Bank v1.9.9.4 (2015-06-12) * Fixes parsing of incoming payments for American Express (thanks to mhagander) * Updates certificates for Nordnet, Avanza, Länsförsäkringar, Forex Bank, OKQ8 and Volvo Finans v1.9.9.3 (2015-05-24) * Updates certificates for Paypal, AmericanExpress, Svenska spel v1.9.9.2 (2015-04-16) * Updates certificates for AmericanExpress, MinPension, Swedbank and Sparbankerna * Adapt to new URLs used by Ticket Rikskortet * Fixes application crash for Samsung devices running Android 4.2 v1.9.9.1 (2015-04-09) * Fixes application crash for Jojo accounts. * Removes support for DinersClub due to BankId requirement. * Fixes login issues with some PayPal accounts. v1.9.9.0 (2015-04-02) * Adds support for minpension.se. * Adds option to disable auto updates while roaming. * Removes Skandiabanken due to their new BankId requirement. * Fixes issue with Nordnet (thanks to jonasgroth). * Fixes crash when account is not available from widget. * Fixes transactions for Forex Bank (thanks to jonasgroth). * Fixes transaction issues for EspressoHouse. * Fixes Payson balance. NOTE: Transactions not supported yet (thanks to robho). * Fixes LockPattern crashes for Samsung Galaxy devices. * Fixes issue with PayPal (now even supporting WWW login and transactions). * Updates certificates for IkanoPartners * Removes support for Sparbanken Öresund due to BankId requirement. v1.9.8.0 (2015-02-27) * Adds support for Bredband2's VoIP service (thanks to fredrike) * Adds support for EspressoHouse (thanks to fredrike) * Adds support for multiple travel cards for JoJo Reskassa * Sets subaccounts for Avanza to hidden by default (thanks to fredrike) * Order transactions by descending date * Fixes www login for Blekingetrafiken (thanks to fredrike) * Fixes www login for Avanza (thanks to fredrike) * Fixes date formatting for VolvoFinans * Fixes application crash for Coop. * Fixes application crash when setting LockPattern on some devices * Fixes support for Nordea accounts with number in the account name * Fixes Swedbank and Sparbankerna if you have corporate credit card * Fixes www login for Coop * Fixes application crash if SonyEricsson's LiveView integration is enabled * Fixes application crash if screen is rotated during a bank update * Improved error handling * Disabled www-button for Swedbank and Sparbankerna * Updated certificate for Östgötatrafiken, AmericanExpress, Brummer KF * Removes support for Handelsbanken, SEB, Chevrolet, Djurgarden, EurobonusMastercard, EurobonusMastercardDk, EurobonusMastercardNo, Eurocard, Opel, Quintessentially, SJPrio, Saab, Statoil, Wallet because they now requires BankId. v1.9.7.4: (2015-02-21) * Updated certificates for ICA Banken, Västtrafik, Brummer KF, Diners Club, Ikano Bank, Marginalen, Trustbuddy, Zidisha, AmericanExpress, TicketRikskortet * Adds support for Coop's MedMera-Före and MedMera-Efter (thanks to fredrike) * Adds support for Blekingetrafiken (thanks to fredrike) * Adds support for Östgötatrafiken (thanks to robho) * Adds support for point account and transactions for Coop (thanks to fredrike) * Adds support for Coop's MedMera Efter credit card (thanks to fredrike) * Adds possibility to go back to the account view from the transaction view by clicking on the logotype (thanks to fredrike) * Use SvenskaSpel's API (thanks to mikaeler) * Fix for null currency for credit cards for Swedbank and Sparbankerna. Defaults to SEK. * Fix Handshake failed errors for FirstCard and PostGirot * Fix for credit card currency error for Nordea (thanks to Ree) * Fix for Västtrafik (thanks to jonasgroth) * Fix balance for Avanza (thanks to fredrike) v1.9.7.3: (2014-11-21) * Fix for Nordea balance (thanks to sed) * Fix for widget sizes v1.9.7.2: (2014-11-20) * Quickfix for widgets, still work to be done * Improved account order for Swedbank (thanks to goober) v1.9.7.1: (2014-11-19) * Swedbank and Sparbankerna: Show transactions, fix for widget, credit card transactions, various fixes (thanks to goober, cortex, mikaeler) * Updated certificate for American Express, Brummer KF, CSN, EasyCard, FirstCard, ICA, Jojo, Meniga, Nordea DK, PlusGirot, Svenskaspel and Västtrafik (thanks to goober, Niclasl, auno) * Account balance for Nordea credit cards (thanks to sed03) * Fix for Jojo (thanks to sed03) * Toggle account visibility by long pressing an account (thanks to fredrike) * Show all account types for Avanza (thanks to fredrike) v1.9.7.0: (2014-10-31) * Fix for Nordea (thanks to rhoot and wicol) * Use Swedbank's API (thanks to goober for the implementation and Swedbank for giving us access to their API) * Updated SSL certificate for Skandiabanken * Fix for Chalmrest balance (thanks to emilan) v1.9.6.16: (2014-09-18) * Updated SSL certificates for American Express, Coop, Eurocard, Forex Bank, Ikano Bank, Marginalen Bank, Meniga, Nordnet, Nordnet Direkt, OKQ8, Payson, Rikslunchen, SevenDay, Trustbuddy and Villabanken * Improvements to Villabanken account parsing (thanks to knifhen) * Improvements to Coop v1.9.6.15: (2014-06-07) * Fix for Coop MedMedmera Visa and Coop Kort * Updated SSL certificate for SEB cards (SAS Eurobonus, SJPrio, Statoil and more) * Allow all characters when entering username for PlusGirot * Updated SevenDay SSL certificate * Improvements to ResursBank * Login fix for Ikano Partner cards (Ikea, Preem, Shell, Skoda and more) v1.9.6.14: (2014-05-13) * Fix for ICA Banken * Fix for Payson * Partial fix for Coop + Support for Sveadirekt (thanks to goober) + Support for Supreme Card v1.9.6.13: (2014-04-30) * Prevent recent apps thumbnail for pattern lock and settings * Updated SSL certificate for American Express, Coop, CSN, Handelsbanken, Osuuspankki, PayPal, Resurs Bank, Sevenday and Zidisha. v1.9.6.12: (2014-04-14) * Updated SSL certificate for Nordea v1.9.6.11: (2014-04-10) * Updated SSL certificate for Swedbank * Transactions working for ICA accounts * Fix for ICA Banken v1.9.6.10: (2014-03-26) * Updated SSL certificate for Akelius Invest * Fix ICA for accounts with transactions v1.9.6.9: (2014-03-24) * Updated SSL certificate for Avanza * Use Avanza implementation for Avanza Mini * Update ICA implementation to use internal ICA API v1.9.6.8: (2014-03-20) * Updated SSL certificate for American Express * Fix for PayPal * Fix for ICA Banken (new api-key) v1.9.6.7: (2014-02-21) * Fix login fields parsing for Nordnet * Fix for regular Jojo cards * Use FLAG_SECURE for ICS and newer to prevent thumbnail in recent tasks list * Updated SSL certificates for American Express and Swedbank v1.9.6.6: (2014-01-24) * Fix for Lunchkortet * Fix for Jojo * Updated SSL certificates for Ticket Rikskortet, Paypal, Avanza, Avanza Mini and Payson v1.9.6.5: (2013-12-19) * Correct currency for SAS Eurobonus Mastercard Norway and Denmark * Fix for ICA Banken (new api-key) v1.9.6.4: (2013-12-13) * Fix for SAS Eurobonus Mastercard Norway * Fix for SEB Cards (Eurocard, SAS Eurobonus, Statoil, SJ Prio...) with multiple accounts * Fix for Bitcoin where transfers had notes * Fix for ICA Banken v1.9.6.3: (2013-12-05) * Fix for American Express * Fix for Eurocard * Fix for SAS Eurobonus Mastercard * Fix for Statoil Mastercard * Fix for Bioklubben * Fix for SJPrio * Various bugfixes v1.9.6.2: (2013-11-03) * Fix for Avanza (thanks to NanoRage) * Fix for Ikano Bank * Improved security of secure connection with certificate pinning + Support for Eurobonus Mastercard Denmark + Support for Bitcoin v1.9.6.1: (2013-09-30) * Fix for PayPal * Fix for Diners Club (thanks to ScuttleSE) * ICA Banken uses their API (thanks to goober) * Fix for multiple accounts in Forex Bank (thanks to rickythefox) * Fix for Villabanken (thanks to knifhen) + Support for SAS EuroBonus MasterCard Norway - Removed Danske Bank - Removed Sparbanken Öresund v1.9.6.0: (2013-06-22) * Fix for PayPal * Fix for Hemköp * Fix for ICA * Fix for Volvofinans + Support for Forex Bank (thanks to rickythefox) + Support for BetterGlobe (thanks to Tuxie) + Support for Zidisha (thanks to Tuxie) v1.9.5.4: (2013-04-28) * Fix for Rikslunchen (thanks to chrfle) * Fix for Audikortet (maybe) + Support for TrustBuddy (thanks to Tuxie) + Support for Brummer Privat KF (thanks to Tuxie) v1.9.5.3: (2013-04-02) * Fix for Skodakortet * Fix for Volvofinans transactions * Fix for reappearing Akelius Spar/Invest accounts v1.9.5.2: (2013-03-27) * Fix for Coop * Show multiple Nordnet accounts v1.9.5.1: (2013-02-26) * Fix for Länsförsäkringar saving accounts v1.9.5: (2013-02-21) * Fix for Länsförsäkringar + Support for Svenska Spel (thanks to nixi) + Support for Appeak Poker v1.9.4: (2013-01-04) * Fix for ICA Banken transactions (thanks to fruktsallad) * Fix for Länsförsäkringar * Fix for Skandiabanken accounts without transactions * Fix for Amex transactions (thanks to goober) * Fixes for Marginalen Bank (thanks to jsiverskog) v1.9.3: (2012-11-29) + Support for Marginalen Bank (thanks to jsiverskog) * Fix for password saving in Android 4.2+ (thanks to gfu) * Fix for ICA (thanks to oskla129) * Fix reappearing accounts on ICA Banken v1.9.2: (2012-10-21) * Fix for Länsförsäkringar - Now uses the same API as their official app v1.9.1: (2012-10-11) + Support for Chalmrest (thanks to emilan) * Fix for Västtrafik (thanks to erifre) * Fix for Nordea captcha - login as many times as you want with the built in captcha breaker * Fix for Bioklubben * Fix for PayPal * Better auto updates with SEB * Fix for EverydayCard (thanks to d95andek) * Fix for Volvofinans (thanks to d95andek) - Removed support for Steam Wallet v1.9.0: (2012-06-25) + Support for Ticket Rikskortet + Support for Bioklubben * Fix for Meniga (thanks to bjooork) * Fix for Nordea DK (thanks to goober) * Fix for Västtrafik (thanks to erifre) * Fix for Statoil Mastercard * Fix for SAS Eurobonus Mastercard * Fix for Eurocard v1.8.9: (2012-04-16) + New setting: Number of simultaneous notifications * Fix for graphical bug on devices running older versions of Android (sorry for the poor testing before release) v1.8.8: (2012-04-13) + Support for Meniga (thanks to bjooork) + New notification setting: Display change only + Disable thumbnail in recent apps list (Honeycomb, ICS) * Fix for Skandiabanken, now using their internal JSON-api (thanks to woody) * Fix for Jojo (thanks to wanders) * Fix for Rikslunchen (thanks to mafa73) * Fix for IKEA v1.8.7: (2012-01-29) * Fix for SEB * Fix for double lock pattern when opening transactions view from widget v1.8.6: (2012-01-29) * Fix for SEB * Fix for LED color picker on pre ICS devices v1.8.5: (2012-01-27) + Support for LED notifications + Support for Akelius Invest * Fix for Coop * Fix for Hemköp * Fix for SEB * Fix for SevenDay * Fix for ICA v1.8.4: (2011-12-04) * Fix for Coop * Fix for Länsförsäkringar (thanks to MatsKarlsson!) + Support for Everydaycard (thanks to d95andek!) + Hardware acceleration is now activated for Honeycomb and ICS * Smarter automatic updates v1.8.3: (2011-11-13) + Support for American Express + Support for Västtrafik * Fix for SEB v1.8.1: (2011-09-26) + Support for Danske Bank + Support for Nordea (Danmark) (thanks to goober!) * Fix for Skandiabanken (thanks to woody!) v1.8.0: (2011-08-24) + Support for AudiKortet + Support for Chevrolet Big Plus Card + Support for Djurgårdskortet + Support for IKEA HANDLA Kort + Support for Nordnetdirekt + Support for PlusGirot + Support for Preem Privatkort + Support for Quintessentially Credit Card + Support for SaabKortet + Support for Seatkortet + Support for Shell MasterCard + Support for Skandiabanken (thanks to woody) + Support for Skodakortet + Support for Volkswagenkortet + Support for wallet MasterCard * Fix for ICA + Support for Sony Ericcson LiveView notifications (thanks to firetech) + New setting: Only update between user defined times (thanks to woody) * Fix for security bug where a user could circumvent the password protection * Calculate the total amount correctly + Donate button added v1.7.3 (2011-05-04) * Fix for Jojo + Support for OpelKortet + Support for SJ Prio MasterCard + Support for Sparbanken Syd + Support for Sparbanken Öresund + Show old CSN loans + Multi-bank support for Swedbank + New setting: Display balance without decimals + New setting: Smallest change to trigger notification v1.7.2: (2011-04-14) * Fix for Swedbank transactions (update account from app to see transactions) * Fix for Länsförsäkringar + New setting: Update transaction history from widget v1.7.1: (2011-04-12) * Fix for Swedbank * For for Payson when using multiple currencies * Support for multiple currencies in DinersClub * Improved error handling for CSN + Progress bar in webview + Bank icons when choosing a bank + Setting: Disable visible pattern on pattern lock v1.7.0: (2011-03-25) + Support for SevenDay + Support for Nordnet + Support for Osuuspankki/Andelsbanken + Support for Volvofinans + Support for Resurs Bank + Support for Länsförsäkringar Pension + Support for McDonald's Presentkort + New setting: Open main view or transactions view from widget + Loans information for Swedbank + Support for additional currencies for Nordea * Repayments to CSN are shown as transactions * Fixed FC-bug in Länsförsäkringar if user had pension account * Sort and display all transactions for SEB * New logo for Ikano Bank (after complaints from Ikano that the old one did not follow their graphic guidelines) * Fix for FirstCard * Sorted transactions for Statoil and Eurobonus * Show all transactions for Eurocard * "Home" button clears history stack v1.6.3: (2011-02-06) * FC fix when opening settings v1.6.2: (2011-02-05) + Support for SEB (automatic website login doesn't work yet) + Support for SAS EuroBonus Mastercard + Support for Rikslunchen + Support for Hemköp Kundkort + Transactions for Diners Club * PayPal business accounts are now supported + Content Provider - Lets other apps use data from Bankdroid after user confirmation v1.6.1: (2010-12-29) * Fix for FC when using pattern lock in landscape mode * Larger hit box on "Home" button v1.6.0: (2010-12-29) + Login to any of your banks website automatically by clicking the WWW-button. * Design changes * Eurocard widgetbug fixed * Fix for Ikano Bank v1.5.3: (2010-12-21) + Support for Ikano Bank + MedMera Faktura for Coop * Multiple accounts for Eurocard v1.5.2: (2010-12-18) + Support for Diners Club + Transactions for Länsförsäkringar v1.5.1: (2010-12-15) + Funds and loans for Länsförsäkringar * MedMera Visa for Coop fixed * Fix for Handelsbanken * Password field show numeric keyboard if appropriate v1.5.0: (2010-12-12) + Support for Jojo Reskassa + Support for Steam Wallet + Support for Avanza (again...) + New setting: Round balance on widgets + Clicking a widget shows transactions for that account. * Additional account information for OKQ8 and Statoil * Fix for ICA Banken * Fix for Coop v1.4.4: (2010-11-26) + Support for Payson * Länsförsäkringar fixed * Coop login problems fixed/improved + New settings: Blur account balance on widgets v1.4.3: (2010-11-19) - Support for Avanza removed * PayPal is working again * Show all transactions for Statoil * Fix for EuroCard * Fix for First Card * Hidden accounts are not counted towards the total sum * Replaced password lock with pattern lock + Support for Remote Notifier plugin + Support for OpenWatch plugin + Custom notification sound + New setting: Don't update transactions on automatic updates v1.4.2: (2010-11-03) * Fixed username field for Avanza v1.4.1: (2010-11-03) * Fix for Nordea v1.4.0: (2010-11-02) + Support for OKQ8 (thanks to cola) + Support for PayPal + Support for Eurocard with transactions + Support for First Card with transactions * Avanza was split into Avanza and AvanzaMini + Nordea credit cards (Visa Gold and others) + Transactions for Coop MedMera Visa * Statoil Mastercard fixed * Transactions for Handelsbanken fixed * A bit faster updates by skipping an unnecessary login when fetching transactions (thanks to DEGE) * Notifications show changes for accounts, not banks. + Encryption for passwords + Option to hide accounts + Option to disable notifications for accounts + New setting: Choose which notifications to display (funds, loands, credit cards) * Every bank has a pre defined keyboard layout (numeric for Nordea, email for PayPal) * Loans are not counted towards the total bank sum v1.3.1: (2010-09-20) * Fix for Swedbank * Fix for Avanza + Support for Avanza mini + Support for Villabanken v1.3.0: (2010-09-19) * Widgets with Handelsbanken accounts are working again * Transactions for ICA Banken * Coop now also works with "MedMera-kortet" * Transactions for Coop MedMera * Transactions for Handelsbanken * Hit box for updating from widgets is larger * Support for loans in Swedbank * Support for funds and loans in Nordea * Widget with transparent background + Support for ICA Kortet with transactions + Support for Statoil MasterCard with transactions + Support for Avanza v1.2.1: (2010-07-13) * Added missing ICA Banken logo * Fix amount formatting v1.2.0: (2010-07-12) * Fixed Nordea bug (login failed when user had an "e-faktura" waiting) * New design * Confirm password field + Link to the banks website + Support for Handelsbanken + Support for Coop + Transaction history for Nordea + Transaction history for Swedbank * Improved login error detection + Support for QVGA devices v1.1.4: (2010-06-03) * Fix for Nordea v1.1.3: (2010-06-02) + New widget size, 2x1 * Widgets are updated on boot v1.1.2: (2010-05-31) * Working fix for ICA Banken v1.1.1: (2010-05-31) * Fix for FC bug with widgets * Fix for ICA Banken v1.1.0: (2010-05-31) + Widgets + Automatic updates + Notifications on account changes + Support for Länsförsäkringar + Improved application lock v1.0.3: (2010-05-16) + Option to password protect the application v1.0.2: (2010-05-12) * Fix for Swedbank v1.0.1: (2010-05-10) + Support for ICA Banken v1.0.0: (2010-05-05) + First public release + Support for Nordea + Support for Swedbank ================================================ FILE: CONTRIBUTING.md ================================================ # Contribute to Bankdroid This guide details how to use issues and pull requests (for new code) to improve Bankdroid. ## Code Style and Template We use the [Android Open-Source Project (AOSP) Code Style Guidelines](http://source.android.com/source/code-style.html). We strongly suggest that you use the [`AndroidStyle.xml`](/config/ide/androidstudio/AndroidStyle.xml) template file, included in this repository, in Android Studio to format your code: 1. Place `AndroidStyle.xml` in your Android Studio `/codestyles` directory (e.g., `%userprofile%\.AndroidStudio\config\codestyles`, or `~/Library/Preferences/AndroidStudio/codestyles/` on OS X) 2. Restart Android Studio. 3. Go to "File->Settings->Code Style", and under "Scheme" select "AndroidStyle" and click "Ok". 4. Right-click on the files that contain your contributions and select "Reformat Code", check "Optimize imports", and select "Run". ## Closing policy for issues and pull requests Bankdroid is a popular project and the capacity to deal with issues and pull requests is limited. Out of respect for our volunteers, issues and pull requests not in line with the guidelines listed in this document may be closed without notice. Please treat our volunteers with courtesy and respect, it will go a long way towards getting your issue resolved. Issues and pull requests should be in English and contain appropriate language for audiences of all ages. ## Issue tracker The [issue tracker](https://github.com/liato/android-bankdroid/issues) is only for obvious bugs, misbehavior, & feature requests in the latest stable or development release of Bankdroid. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a pull request which partially or fully addresses the issue. ### Issue tracker guidelines **[Search](https://github.com/liato/android-bankdroid/search?q=&ref=cmdform&type=Issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature request. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post) and feature requests in a similar format: 1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) 2. **Steps to reproduce:** How can we reproduce the issue? 3. **Expected behavior:** What did you expect the app to do? 4. **Observed behavior:** What did you see instead? Describe your issue in detail here. 5. **Device and Android version:** What make and model device (e.g., Samsung Galaxy S3) did you encounter this on? What Android version (e.g., Android 4.0 Ice Cream Sandwich) are you running? Is it the stock version from the manufacturer or a custom ROM? 5. **Screenshots:** Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. 6. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem. ## Pull requests We welcome pull requests with fixes and improvements to Bankdroid code, tests, and/or documentation. The features we would really like a pull request for are open issues with the [feature](https://github.com/liato/android-bankdroid/issues?labels=feature&page=1&state=open), [broken](https://github.com/liato/android-bankdroid/issues?labels=broken&page=1&state=open) or [improvement](https://github.com/liato/android-bankdroid/issues?labels=improvement&page=1&state=open) labels. ### Pull request guidelines If you can, please submit a pull request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a pull request is as follows: 1. Fork the project on GitHub 2. Create a feature branch 3. Write tests and code 4. Apply the `AndroidStyle.xml` style template to your code in Android Studio. 5. Add your changes to the [CHANGELOG](/CHANGELOG) 6. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) 7. Push the commit to your fork 8. Submit a pull request with a motive for your change and the method you used to achieve it 9. [Search for issues](https://github.com/liato/android-bankdroid/search?q=&ref=cmdform&type=Issues) related to your pull request and mention them in the pull request description or comments We will accept pull requests if: * The code has proper tests and all tests pass (or it is a test exposing a failure in existing code) * It can be merged without problems (if not please use: `git rebase master`) * It doesn't break any existing functionality * It's quality code that conforms to standard style guides and best practices * The description includes a motive for your change and the method you used to achieve it * It is not a catch all pull request but rather fixes a specific issue or implements a specific feature * It keeps the Bankdroid code base clean and well structured * We think other users will benefit from the same functionality * If it makes changes to the UI the pull request should include screenshots * It is a single commit (please use `git rebase -i` to squash commits) ## License By contributing code to this project via pull requests, patches, or any other process, you are agreeing to license your contributions under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). ================================================ FILE: CONTRIBUTORS.txt ================================================ -------------------------------------------------------------------------------- Development -------------------------------------------------------------------------------- liato https://github.com/liato DEGE1 https://github.com/DEGE1 Magnus Hagander https://github.com/mhagander Pierre Chateau https://github.com/PMC Magnus Andersson https://github.com/magnusart cork https://github.com/cork Emil Hesslow https://github.com/WizKid Peter Björkman https://github.com/woody2 Joakim Andersson https://github.com/firetech Mathias Åhsberg https://github.com/goober Lars Wiklund https://github.com/lawi75 Fredrik Lindberg https://github.com/fredriklindberg Erik Fredriksen https://github.com/erifre Mats Karlsson https://github.com/MatsKarlsson Snah https://github.com/Snaah Jonas Björk https://github.com/bjooork Anders Waldenborg https://github.com/wanders Mattias Fagerström https://github.com/mafa73 Jacob Siverskog https://github.com/jsiverskog Andreas Gunnerås https://github.com/d95andek Emil Andersson https://github.com/emilan oskla129 https://github.com/oskla129 Per Wigren https://github.com/Tuxie Joakim Lundborg https://github.com/cortex Jonathan Hjertström https://github.com/nixi Tim Jansson https://github.com/timtux Christer Fletcher https://github.com/chrfle Richard Ginzburg https://github.com/rickythefox ScuttleSE https://github.com/ScuttleSE Andreas Knifh https://github.com/knifhen NanoRage https://github.com/NanoRage Adam Nybäck https://github.com/nadam MathiasBois https://github.com/MathiasBois Johan Sköld https://github.com/rhoot Wictor https://github.com/wicol sed03 https://github.com/sed03 Fredrik Erlandsson https://github.com/fredrike Niclasl https://github.com/NiclasI Mikael Eriksson https://github.com/mikaeler Mikael Auno https://github.com/auno robho https://github.com/robho Johan Walles https://github.com/walles Stats at: https://github.com/liato/android-bankdroid/graphs/contributors?type=a -------------------------------------------------------------------------------- Beta testing -------------------------------------------------------------------------------- Swedroid users http://goo.gl/9tJeH ================================================ FILE: LICENSE ================================================ Copyright 2015 liato Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.rst ================================================ .. image:: https://travis-ci.org/liato/android-bankdroid.svg?branch=master :target: https://travis-ci.org/liato/android-bankdroid .. image:: https://bankdroid.herokuapp.com/badge.svg :alt: Join the chat at https://bankdroid.herokuapp.com/ :target: https://bankdroid.herokuapp.com/ Bankdroid ========= Bankdroid is an Android app for Swedish banks, payment cards and similar services. Key features include: * Automatic updates of your balance and transactions * Notifications whenever your balance changes * Widgets to show your balance anywhere on your home screen More information can be found at: * `Bankdroid on Google Play `_ * `Bankdroid thread at the Swedroid Forum `_ (Swedish) Contribute or report broken banks ------------------------------------ The following information is needed for troubleshooting a broken bank or if you want a new bank to be supported by Bankdroid. 1. Address to login page. 2. Address and html code for the landing page after a successful login. 3. Address and html code for the page with the accounts overview. 4. Address and html code for the page with an account's transaction history. NOTE. Do not forget to replace your personal information in the html code with random information before you send everything to android [at] nullbyte.eu. You can also open an issue here at Github with the required files included as an attachment. Development environment ----------------------- Bankdroid is written for Android Studio 0.8.2 and Gradle 1.12. Here's how to get the code up and running on your computer: 1. Make sure you have `Android Studio 0.8.2 or later `_ 2. `Clone `_ the project (if you want to contribute you should `fork `_ the project first and then clone your fork) 3. Open the project's settings.gradle file in Android Studio and select "Use default gradle wrapper (recommended)" 4. Select "Make project" from the Build menu 5. Select Run from the Run menu License ------- The Bankdroid source code is licensed under the `Apache License, Version 2.0 `_. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ buildscript { repositories { maven { url 'https://maven.fabric.io/public' } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "org.ajoberstar:gradle-git:1.5.1" // The dynamic version here is explicitly recommended by the Crashlytics docs: // https://docs.fabric.io/android/fabric/integration.html#modify-build-gradle //noinspection GradleDynamicVersion classpath 'io.fabric.tools:gradle:1.+' } } apply plugin: 'com.android.application' apply from: '../config/quality/quality.gradle' apply plugin: "org.ajoberstar.grgit" if (new File('app/crashlytics.properties').exists()) { apply plugin: 'io.fabric' } repositories { maven { url 'https://maven.fabric.io/public' } } def keystorePropertiesFile = project.file("keystore.properties") def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) ext { git = org.ajoberstar.grgit.Grgit.open() gitVersionCode = git.tag.list().size() gitVersionName = "${git.describe()}" } android { compileSdkVersion 25 buildToolsVersion "25.0.1" useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "com.liato.bankdroid" minSdkVersion 9 targetSdkVersion 25 versionCode 224 + gitVersionCode versionName gitVersionName } signingConfigs { release { storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } packagingOptions { exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } debug { ext.enableCrashlytics = false } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':bankdroid-legacy') compile project(':bankdroid-core') compile 'com.jakewharton:butterknife:6.1.0' compile 'com.jakewharton.timber:timber:4.3.1' compile "com.android.support:appcompat-v7:25.2.0" compile 'com.google.collections:google-collections:1.0' compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { transitive = true; } testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' } ================================================ FILE: app/crashlytics.properties.example ================================================ apiSecret=YOUR_BUILD_SECRET apiKey=YOUR_API_KEY ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /home/liato/bin/android-studio/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -keep class com.crashlytics.** { *; } -dontwarn com.crashlytics.** # Allow obfuscation of android.support.v7.internal.view.menu.** # to avoid problem on Samsung 4.2.2 devices with appcompat v21 # see https://code.google.com/p/android/issues/detail?id=78377 -keep class !android.support.v7.internal.view.menu.**,android.support.** {*;} #Butterknife -keep class butterknife.** { *; } -dontwarn butterknife.internal.** -keep class **$$ViewInjector { *; } -keepclasseswithmembernames class * { @butterknife.* ; } -keepclasseswithmembernames class * { @butterknife.* ; } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/aidl/com/sonyericsson/extras/liveview/IPluginServiceCallbackV1.aidl ================================================ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.sonyericsson.extras.liveview; interface IPluginServiceCallbackV1 { // Start the plugin. void startPlugin(); // Stop the plugin. // A stopped plugin should stop its polling, but can stay alive void stopPlugin(); // Should return the name the plugin used to register itself with String getPluginName(); // Give the action needed to open the current announcement on the phone // such as a view in browser action or something else that your application // responds to. void openInPhone(in String openInPhoneAction); // Kicked out by framework. Implement this with stopSelf() void onUnregistered(); // displayWidthPixels equals 0 and displayheigthPixels equals 0 // means no available device is attached, or has no display void displayCaps(in int displayWidthPixels, in int displayHeigthPixels); // Button event - note that doublepress is not implemented for the V1 // interface but still left here for compatibility reasons. void button(in String buttonType, in boolean doublepress, in boolean longpress); // Screen mode changed event - this notifies the current active sandbox plugin that the screen has been // turned on or off. 0 = Screen OFF, 1 = Screen ON void screenMode(in int screenMode); } ================================================ FILE: app/src/main/aidl/com/sonyericsson/extras/liveview/IPluginServiceV1.aidl ================================================ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.sonyericsson.extras.liveview; import com.sonyericsson.extras.liveview.IPluginServiceCallbackV1; interface IPluginServiceV1 { // Register to the Liveview application // cb - callback instance // imageMenu - path to the menu bitmap // pluginName - name of the plugin - must be unique // selectableMenu - is set to true if controlling display and getting buttons. Set to false to only handle announces // packageName - the package name (use getPackageName()). // returns id of plugin in system, 0 means that the registration failed int register(in IPluginServiceCallbackV1 cb, in String imageMenu, in String pluginName, in boolean selectableMenu, in String packageName); // This method should be called if the application/service is uninstalled using the phone application handler // id - the plugin id received in registerPlugin // cb - the callback void unregister( in int id, in IPluginServiceCallbackV1 cb); // Used to send announcements to the device - can only be used when _not_ registered as "selectableMenu" // id - the plugin id received in registerPlugin // imageAnnounce - the path to the announce bitmap // header - header text // body - body text // time - timestamp for this announce in milliseconds // openInPhoneAction - a tag to use for openInPhone callback. Set to null when announce not selectable void sendAnnounce(in int id, in String imageAnnounce, in String header, in String body, in long timestamp, in String openInPhoneAction); // Used to send image data to the device while in sandbox mode - Can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin // x - from left side // y - from top side // image - the path to the image on file system void sendImage(in int id, in int x, in int y, in String image); // Used to send image data to the device while in sandbox mode - Can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin // x - from left side // y - from top side // bitmapData - the bitmap to send void sendImageAsBitmap(in int id, in int x, in int y, in Bitmap bitmapData); // Clears the display, for example if several images are sent while in sandbox mode - Can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin void clearDisplay(in int id); // Provide the Liveview application with means to launch the service // that shoul receive and send data in sandbox mode - Must be called if you registered as "selectableMenu" // launcherIntent - the intent to start the plugin service // pluginName - the name of the plugin, must match the name you registered with! // returns -1 for failure, 0 for already registered, anything else for success int notifyInstalled(in String launcherIntent, in String pluginName); // Controls LED - can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin // rgb565 - the color to use // delayTime - the delay in ms // onTime - the on time in ms void ledControl(in int id, int rgb565, int delayTime, int onTime); // Controls Vibration - can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin // delayTime - the delay in ms // onTime - the on time in ms void vibrateControl(in int id, int delayTime, int onTime); // Used to send image data to the device while in sandbox mode - Can only be used if you registered as "selectableMenu" // id - the plugin id received in registerPlugin // x - from left side // y - from top side // bitmapBytes - the byteArray containing the bitmap data void sendImageAsBitmapByteArray(in int id, in int x, in int y, in byte[] bitmapByteArray); // Used to put the screen in powersave mode // id - the plugin id received in registerPlugin void screenOff(in int id); // Used to put the screen in dimmed mode // id - the plugin id received in registerPlugin void screenDim(in int id); // Used to wake the screen from powersave mode // id - the plugin id received in registerPlugin void screenOn(in int id); // Used to set the to powersave mode "AUTO" // id - the plugin id received in registerPlugin void screenOnAuto(in int id); } ================================================ FILE: app/src/main/java/com/ast/util/CookieParser.java ================================================ /* * Copyright (C) 2009 hessdroid@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ast.util; /** * Parses Set-Cookie HTTP response header and returns the corresponding {@link Cookie} which can be * used for storing cookie information in java collections. *

* RFC 2109 * * @author hessdroid@gmail.com */ public class CookieParser { /** * Parses the given setCookieString from an HTTP response and creates a {@link Cookie} * from it. * The {@link Cookie} can be used by clients to send cookie on subsequent requests. * * @param host String cookie host * @param setCookieString String complete cookie string * @return a {@link Cookie} that was created from the given setCookieString */ public static Cookie parse(String host, String setCookieString) { if (host == null) { throw new IllegalArgumentException("Parameter \"host\" must not be null"); } if (setCookieString == null) { throw new IllegalArgumentException("Parameter \"setCookieString\" must not be null"); } Cookie result = new Cookie(); // split the cookie string on any semicolon or space(s) String[] fields = setCookieString.split(";\\s*"); result.host = host; // ignore leading cookie intro result.value = fields[0].startsWith("Set-Cookie: ") ? fields[0].substring(12) : fields[0]; if (result.value.startsWith("JSESSIONID=")) { result.sessionId = result.value.substring(11); } // Parse each field for (int j = 1; j < fields.length; j++) { if ("secure".equalsIgnoreCase(fields[j])) { result.secure = true; } else if (fields[j].indexOf('=') > 0) { String[] f = fields[j].split("="); if ("expires".equalsIgnoreCase(f[0])) { result.expires = f[1]; } else if ("domain".equalsIgnoreCase(f[0])) { result.domain = f[1]; } else if ("path".equalsIgnoreCase(f[0])) { result.path = f[1]; } } } return result; } /** * Abstract representation of an HTTP cookie. */ public static class Cookie { public String host; public String value; // cookie string without the leading intro: "Set-Cookie: " public String expires; public String path; public String domain; public String sessionId; public boolean secure; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/AboutActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import android.content.Intent; import android.content.pm.PackageInfo; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.TextView; import butterknife.ButterKnife; import butterknife.InjectView; public class AboutActivity extends LockableActivity { @InjectView(R.id.txtVersion) TextView mVersion; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.about); ButterKnife.inject(this); PackageInfo pInfo; String version = BuildConfig.VERSION_NAME; mVersion.setText( getText(R.string.version).toString().replace("$version", version)); } @Override public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); final MenuInflater inflater = new MenuInflater(this); inflater.inflate(R.menu.about, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_web: Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse("https://github.com/liato/android-bankdroid")); startActivity(i); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/ActivityHelper.java ================================================ package com.liato.bankdroid; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.ContextWrapper; public class ActivityHelper { public static void dismissDialog(Dialog dialog) { if (dialog.isShowing()) { //check if dialog is showing. //get the Context object that was used to great the dialog Context context = ((ContextWrapper) dialog.getContext()).getBaseContext(); //if the Context used here was an activity AND it hasn't been finished //then dismiss it if (context instanceof Activity) { if (!((Activity) context).isFinishing()) { dialog.dismiss(); } } else { //if the Context used wasnt an Activity, then dismiss it too dialog.dismiss(); } } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/BankEditActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.google.common.collect.Iterators; import com.crashlytics.android.Crashlytics; import com.liato.bankdroid.api.configuration.Entry; import com.liato.bankdroid.api.configuration.Field; import com.liato.bankdroid.appwidget.AutoRefreshService; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankChoice; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.configuration.DefaultConnectionConfiguration; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.utils.FieldTypeMapper; import com.liato.bankdroid.utils.NetworkUtils; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.method.PasswordTransformationMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import butterknife.ButterKnife; import butterknife.InjectView; import butterknife.OnClick; import timber.log.Timber; public class BankEditActivity extends LockableActivity implements OnItemSelectedListener { @InjectView(R.id.spnBankeditBanklist) Spinner mBankSpinner; @InjectView(R.id.layoutBankConfiguration) LinearLayout mFormContainer; @InjectView(R.id.txtErrorDesc) TextView mErrorDescription; private Bank selectedBank; private long bankId = -1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bank); ButterKnife.inject(this); this.getWindow() .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); List items = BankFactory.listBanks(this); Collections.sort(items); BankSpinnerAdapter adapter = new BankSpinnerAdapter<>(this, items); mBankSpinner.setAdapter(adapter); Bundle extras = getIntent().getExtras(); if (extras != null) { bankId = extras.getLong("id", -1); if (bankId != -1) { Bank bank = BankFactory.bankFromDb(bankId, this, false); if (bank != null) { mErrorDescription.setVisibility( bank.isDisabled() ? View.VISIBLE : View.INVISIBLE); mBankSpinner.setEnabled(false); mBankSpinner.setSelection(adapter.getPosition(bank)); selectedBank = bank; createForm(selectedBank.getConnectionConfiguration(), DefaultConnectionConfiguration.fields() ); populateForm(bank); } } } mBankSpinner.setOnItemSelectedListener(this); } @OnClick(R.id.btnSettingsOk) public void onSubmit(View v) { if (!validate()) { return; } selectedBank.setProperties(getFormParameters(selectedBank.getConnectionConfiguration())); selectedBank.setCustomName(getFormParameter(DefaultConnectionConfiguration.NAME)); selectedBank.setDbid(bankId); new DataRetrieverTask(this, selectedBank).execute(); } @OnClick(R.id.btnSettingsCancel) public void onCancel(View v) { this.finish(); } @Override public void onItemSelected(AdapterView parentView, View selectedItemView, int pos, long id) { Bank selectedBank = (Bank) parentView.getItemAtPosition(pos); if (this.selectedBank == null || !this.selectedBank.equals(selectedBank)) { this.selectedBank = selectedBank; mFormContainer.removeAllViewsInLayout(); createForm(this.selectedBank.getConnectionConfiguration(), DefaultConnectionConfiguration.fields() ); } } @Override public void onNothingSelected(AdapterView arg) { } private void createForm(List... configurations) { for (List fields : configurations) { for (Field field : fields) { createLabel(field); if (field.getValues().isEmpty()) { createField(field); } else { createSpinner(field); } } } } private void createLabel(Field field) { TextView fieldText = new TextView(this); String label = field.getLabel() + (field.isRequired() ? "" : " " + getString(R.string.optional_field)); fieldText.setText(label); fieldText.setVisibility(field.isHidden() ? View.GONE : View.VISIBLE); mFormContainer.addView(fieldText); } private void createField(Field field) { EditText inputField = new EditText(this); inputField.setHint(field.getPlaceholder()); if (field.isSecret()) { inputField.setTransformationMethod( PasswordTransformationMethod.getInstance()); } else { inputField .setInputType(FieldTypeMapper.fromFieldType(field.getFieldType())); } inputField.setVisibility(field.isHidden() ? View.GONE : View.VISIBLE); inputField.setTag(field.getReference()); mFormContainer.addView(inputField); } private void createSpinner(Field field) { Spinner spinner = new Spinner(this); spinner.setAdapter(new ArrayAdapter(this, android.R.layout.simple_spinner_item, field.getValues())); spinner.setTag(field.getReference()); mFormContainer.addView(spinner); } private void populateForm(Bank bank) { EditText customName = (EditText) mFormContainer.findViewWithTag( DefaultConnectionConfiguration.NAME); customName.setText(bank.getCustomName()); for (Map.Entry property : bank.getProperties().entrySet()) { EditText propertyInput = (EditText) mFormContainer.findViewWithTag(property.getKey()); propertyInput.setText(property.getValue()); } } private Map getFormParameters(List fields) { Map properties = new HashMap<>(); for (Field field : fields) { properties.put(field.getReference(), getFormParameter(field.getReference())); } return properties; } private String getFormParameter(String property) { View propertyView = mFormContainer.findViewWithTag(property); if (propertyView instanceof EditText) { EditText propertyInput = (EditText) propertyView; return propertyInput.getText().toString().trim(); } else if (propertyView instanceof Spinner) { Spinner spinnerProperty = (Spinner) propertyView; Entry entry = (Entry) spinnerProperty.getSelectedItem(); return entry.getKey(); } else { return null; } } private boolean validate() { boolean valid = true; Iterator fields = Iterators.concat(selectedBank.getConnectionConfiguration().iterator(), DefaultConnectionConfiguration.fields().iterator()); while (fields.hasNext()) { Field field = fields.next(); try { field.validate(getFormParameter(field.getReference())); } catch (IllegalArgumentException e) { valid = false; ((EditText) mFormContainer.findViewWithTag(field.getReference())).setError(e.getMessage()); } } return valid; } private class BankSpinnerAdapter extends ArrayAdapter { private LayoutInflater inflater; public BankSpinnerAdapter(Context context, List items) { super(context, R.layout.bank_spinner_item, R.id.txtBank, items); inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.bank_spinner_item, parent, false); } ((TextView) convertView.findViewById(R.id.txtBank)) .setText(((Bank) getItem(position)).getName()); ((ImageView) convertView.findViewById(R.id.imgBank)) .setImageResource(((Bank) getItem(position)).getImageResource()); return convertView; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.bank_spinner_dropdown_item, parent, false); } ((TextView) convertView.findViewById(R.id.txtBank)) .setText(((Bank) getItem(position)).getName()); ((ImageView) convertView.findViewById(R.id.imgBank)) .setImageResource(((Bank) getItem(position)).getImageResource()); return convertView; } } private class DataRetrieverTask extends AsyncTask { private final ProgressDialog dialog = new ProgressDialog(BankEditActivity.this); private Exception exc = null; private Bank bank; private BankEditActivity context; private Resources res; public DataRetrieverTask(BankEditActivity context, Bank bank) { this.context = context; this.res = context.getResources(); this.bank = bank; } protected void onPreExecute() { this.dialog.setMessage(res.getText(R.string.logging_in)); this.dialog.show(); } protected Void doInBackground(final String... args) { try { bank.update(); bank.updateAllTransactions(); bank.closeConnection(); DBAdapter.save(bank, context); // Transactions updated. final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); if (prefs.getBoolean("content_provider_enabled", false)) { final ArrayList accounts = bank.getAccounts(); for (final Account account : accounts) { AutoRefreshService.broadcastTransactionUpdate( getBaseContext(), bank.getDbId(), account.getId()); } } } catch (BankException e) { this.exc = e; Crashlytics.logException(e); } catch (LoginException e) { this.exc = e; } catch (BankChoiceException e) { this.exc = e; } catch (IOException e) { this.exc = e; if (NetworkUtils.isInternetAvailable()) { Crashlytics.logException(e); } } return null; } protected void onPostExecute(final Void unused) { AutoRefreshService.sendWidgetRefresh(context); ActivityHelper.dismissDialog(this.dialog); if (this.exc != null) { AlertDialog.Builder builder = new AlertDialog.Builder(context); if (this.exc instanceof BankChoiceException) { final BankChoiceException e = (BankChoiceException) exc; final String[] items = new String[e.getBanks().size()]; int i = 0; for (BankChoice b : e.getBanks()) { items[i] = b.getName(); i++; } builder.setTitle(R.string.select_a_bank); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { selectedBank.setExtras(e.getBanks().get(item).getId()); new DataRetrieverTask(context, selectedBank).execute(); } }); } else { Timber.w(exc, "Failed getting info from bank"); builder.setMessage(this.exc.getMessage()) .setTitle(res.getText(R.string.could_not_create_account)) .setIcon(android.R.drawable.ic_dialog_alert) .setNeutralButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); } AlertDialog alert = builder.create(); if (!context.isFinishing()) { alert.show(); } } else { context.finish(); } } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/BankdroidApplication.java ================================================ package com.liato.bankdroid; import com.liato.bankdroid.utils.LoggingUtils; import android.app.Application; import android.widget.Toast; public class BankdroidApplication extends Application { private String message = ""; @Override public void onCreate() { super.onCreate(); LoggingUtils.createLogger(this); } public void setApplicationMessage(String messageText) { message = messageText == null ? "" : messageText; } public void showAndDeleteApplicationMessage() { if (!message.isEmpty()) { Toast toast = Toast.makeText(this, message, Toast.LENGTH_LONG); message = ""; toast.show(); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/BetterPopupWindow.java ================================================ package com.liato.bankdroid; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.widget.PopupWindow; /** * This class does most of the work of wrapping the {@link PopupWindow} so it's simpler to use. * * @author qberticus */ public class BetterPopupWindow { protected final ViewGroup parentView; protected final View anchor; private final PopupWindow window; private final WindowManager windowManager; private View root; private Drawable background = null; /** * Create a BetterPopupWindow * * @param anchor the view that the BetterPopupWindow will be displaying 'from' */ public BetterPopupWindow(ViewGroup parentView, View anchor) { this.anchor = anchor; this.parentView = parentView; this.window = new PopupWindow(anchor.getContext()); // when a touch even happens outside of the window // make the window go away this.window.setTouchInterceptor(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { BetterPopupWindow.this.window.dismiss(); return true; } return false; } }); this.windowManager = (WindowManager) this.anchor.getContext() .getSystemService(Context.WINDOW_SERVICE); onCreate(); } /** * Anything you want to have happen when created. Probably should create a view and setup the * event listeners on * child views. */ protected void onCreate() { } /** * In case there is stuff to do right before displaying. */ protected void onShow() { } private void preShow() { if (this.root == null) { throw new IllegalStateException( "setContentView was not called with a view to display."); } onShow(); if (this.background == null) { this.window.setBackgroundDrawable(new BitmapDrawable()); } else { this.window.setBackgroundDrawable(this.background); } // if using PopupWindow#setBackgroundDrawable this is the only values of the width and hight that make it work // otherwise you need to set the background of the root viewgroup // and set the popupwindow background to an empty BitmapDrawable this.window.setWidth(WindowManager.LayoutParams.FILL_PARENT); this.window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); this.window.setTouchable(true); this.window.setFocusable(true); this.window.setOutsideTouchable(true); this.window.setContentView(this.root); } public void setBackgroundDrawable(Drawable background) { this.background = background; } /** * Sets the content view. Probably should be called from {@link onCreate} * * @param root the view the popup will display */ public void setContentView(View root) { this.root = root; this.window.setContentView(root); } /** * Will inflate and set the view from a resource id */ public void setContentView(int layoutResID) { LayoutInflater inflator = (LayoutInflater) this.anchor.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.setContentView(inflator.inflate(layoutResID, null)); } /** * If you want to do anything when {@link dismiss} is called */ public void setOnDismissListener(PopupWindow.OnDismissListener listener) { this.window.setOnDismissListener(listener); } /** * Displays like a popdown menu from the anchor view */ public void showLikePopDownMenu() { this.showLikePopDownMenu(0, 0); } /** * Displays like a popdown menu from the anchor view. * * @param xOffset offset in X direction * @param yOffset offset in Y direction */ public void showLikePopDownMenu(int xOffset, int yOffset) { this.preShow(); this.window.setAnimationStyle(R.style.Animations_PopDownMenu); this.window.showAsDropDown(this.anchor, xOffset, yOffset); } /** * Displays like a QuickAction from the anchor view. */ public void showLikeQuickAction() { this.showLikeQuickAction(0, 0); } /** * Displays like a QuickAction from the anchor view. * * @param xOffset offset in the X direction * @param yOffset offset in the Y direction */ public void showLikeQuickAction(int xOffset, int yOffset) { this.preShow(); this.window.setAnimationStyle(R.style.Animations_GrowFromBottom); int[] location = new int[2]; this.anchor.getLocationOnScreen(location); Rect anchorRect = new Rect(location[0], location[1], location[0] + this.anchor.getWidth(), location[1] + this.anchor.getHeight()); this.root.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); int rootWidth = this.root.getMeasuredWidth(); int rootHeight = this.root.getMeasuredHeight(); int screenWidth = this.windowManager.getDefaultDisplay().getWidth(); int screenHeight = this.windowManager.getDefaultDisplay().getHeight(); int xPos = ((screenWidth - rootWidth) / 2) + xOffset; int yPos = anchorRect.top - rootHeight + yOffset; // display on bottom if (rootHeight > anchorRect.top) { yPos = anchorRect.bottom - yOffset; this.window.setAnimationStyle(R.style.Animations_GrowFromTop); this.root.findViewById(R.id.layPopupCont).setBackgroundResource(R.drawable.popup_bg_up); } this.window.showAtLocation(this.anchor, Gravity.NO_GRAVITY, xPos, yPos); } public void dismiss() { this.window.dismiss(); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/DataRetrieverTask.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.appwidget.AutoRefreshService; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.utils.LoggingUtils; import com.liato.bankdroid.utils.NetworkUtils; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.AsyncTask; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import timber.log.Timber; public class DataRetrieverTask extends AsyncTask { private final ProgressDialog dialog; private final MainActivity parent; private final Resources res; private ArrayList errors; private long bankId = -1; public DataRetrieverTask(final MainActivity parent) { this.parent = parent; this.res = parent.getResources(); this.dialog = new ProgressDialog(parent); } public DataRetrieverTask(final MainActivity parent, final long bankId) { this(parent); this.bankId = bankId; } @Override protected void onPreExecute() { getDialog().setMessage(res.getText(R.string.updating_account_balance) + "\n "); getDialog().setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); getDialog().setCancelable(false); getDialog().show(); } @NonNull protected ProgressDialog getDialog() { return this.dialog; } @Nullable protected Bank getBankFromDb(long bankId, Context parent) { return BankFactory.bankFromDb(bankId, parent, true); } protected List getBanksFromDb(Context parent) { return BankFactory.banksFromDb(parent, true); } protected void saveBank(Bank bank, Context context) { DBAdapter.save(bank, parent); } protected void publishProgress(int zeroBasedBankNumber, @Nullable Bank bank) { String text = ""; if (bank != null) { text = bank.getName() + " (" + bank.getUsername() + ")"; } publishProgress(Integer.toString(zeroBasedBankNumber), text); } protected boolean isContentProviderEnabled() { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(parent); return prefs.getBoolean("content_provider_enabled", false); } @Override protected Void doInBackground(final String... args) { errors = new ArrayList<>(); List banks; if (bankId != -1) { banks = new ArrayList<>(); banks.add(getBankFromDb(bankId, parent)); } else { banks = getBanksFromDb(parent); } getDialog().setMax(banks.size()); int i = 0; for (final Bank bank : banks) { publishProgress(i, bank); if (isListingAllBanks() && bank.isDisabled()) { LoggingUtils.logDisabledBank(bank); continue; } try { bank.update(); bank.updateAllTransactions(); bank.closeConnection(); saveBank(bank, parent); i++; LoggingUtils.logBankUpdate(bank, true); } catch (final BankException e) { this.errors.add(bank.getName() + " (" + bank.getUsername() + ")"); Timber.e(e, "Could not update bank."); } catch (final LoginException e) { this.errors.add(bank.getName() + " (" + bank.getUsername() + ")"); DBAdapter.disable(bank, parent); } catch (BankChoiceException e) { this.errors.add(bank.getName() + " (" + bank.getUsername() + ")"); } catch (IOException e) { this.errors.add(bank.getName() + " (" + bank.getUsername() + ")"); if (NetworkUtils.isInternetAvailable()) { Timber.e(e, "IO error talking to bank"); } } if (isContentProviderEnabled()) { final ArrayList accounts = bank.getAccounts(); for (final Account account : accounts) { AutoRefreshService.broadcastTransactionUpdate(parent, bank.getDbId(), account.getId()); } } } publishProgress(i, null); return null; } private boolean isListingAllBanks() { return bankId == -1; } @Override protected void onProgressUpdate(final String... args) { getDialog().setProgress(Integer.parseInt(args[0])); getDialog().setMessage(res.getText(R.string.updating_account_balance) + "\n" + args[1]); } @Override protected void onPostExecute(final Void unused) { parent.refreshView(); AutoRefreshService.sendWidgetRefresh(parent); ActivityHelper.dismissDialog(getDialog()); if ((this.errors != null) && !this.errors.isEmpty()) { final StringBuilder errormsg = new StringBuilder(); errormsg.append(res.getText(R.string.accounts_were_not_updated)) .append(":\n"); for (final String err : errors) { errormsg.append(err); errormsg.append("\n"); } final AlertDialog.Builder builder = new AlertDialog.Builder(parent); builder.setMessage(errormsg.toString()) .setTitle(res.getText(R.string.errors_when_updating)) .setIcon(android.R.drawable.ic_dialog_alert) .setNeutralButton("Ok", new DialogInterface.OnClickListener() { @Override public void onClick( final DialogInterface dialog, final int id) { dialog.cancel(); } }); final AlertDialog alert = builder.create(); alert.show(); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/LockableActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.lockpattern.ConfirmLockPattern; import com.liato.bankdroid.lockpattern.LockPatternUtils; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.WindowManager; public class LockableActivity extends ActionBarActivity { private static final int PATTERNLOCK_UNLOCK = 42; protected boolean mSkipLockOnce = false; private SharedPreferences mPrefs; private LockPatternUtils mLockPatternUtils; private boolean mHasLoaded = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mLockPatternUtils = new LockPatternUtils(this); mLockPatternUtils .setVisiblePatternEnabled(mPrefs.getBoolean("patternlock_visible_pattern", true)); mLockPatternUtils.setTactileFeedbackEnabled( mPrefs.getBoolean("patternlock_tactile_feedback", false)); // requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override public void setContentView(int layoutResID) { super.setContentView(layoutResID); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); if (toolbar != null) { if (shouldShowActionBar()) { setSupportActionBar(toolbar); toolbar.setLogo(R.drawable.ic_launcher); } else { toolbar.setVisibility(View.GONE); } } // getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.title); // View titlebar = findViewById(R.id.layTitle); // mTitlebarButtons = (LinearLayout)titlebar.findViewById(R.id.layTitleButtons); // mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); // // mHomeButton = (ImageView)titlebar.findViewById(R.id.imgTitle); // mHomeButtonCont = titlebar.findViewById(R.id.layLogoContainer); // mProgressBar = (ProgressBar)titlebar.findViewById(R.id.progressBar); // OnClickListener listener = new View.OnClickListener() { // public void onClick(View v) { // Intent intent = new Intent(LockableActivity.this, MainActivity.class); // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // startActivity(intent); // LockableActivity.this.finish(); // } // }; // mHomeButton.setOnClickListener(listener); // mHomeButtonCont.setOnClickListener(listener); // setHomeButtonEnabled(true); } // protected void addTitleButton(int imageResourceId, String tag, OnClickListener listener) { // View child = mInflater.inflate(R.layout.title_item, mTitlebarButtons, false); // ImageButton button = (ImageButton)child.findViewById(R.id.imgItemIcon); // button.setImageResource(imageResourceId); // button.setTag(tag); // child.setTag("item_"+tag); // button.setOnClickListener(listener); // mTitlebarButtons.addView(child); // } // // protected void hideTitleButton(String tag) { // View v = mTitlebarButtons.findViewWithTag("item_"+tag); // if (v != null) { // v.setVisibility(View.GONE); // } // } // // protected void showTitleButton(String tag) { // View v = mTitlebarButtons.findViewWithTag("item_"+tag); // if (v != null) { // v.setVisibility(View.VISIBLE); // } // } // // protected void setTitleButtonEnabled(String tag, boolean enabled) { // View v = mTitlebarButtons.findViewWithTag("item_"+tag); // if (v != null) { // ImageButton button = (ImageButton)v.findViewById(R.id.imgItemIcon); // if (button != null) { // v.setEnabled(enabled); // v.setFocusable(enabled); // button.setEnabled(enabled); // button.setAlpha(enabled ? 255 : 50); // } // } // } // // protected void setHomeButtonEnabled(boolean enabled) { // mHomeButtonCont.setFocusable(enabled); // mHomeButtonCont.setClickable(enabled); // mHomeButton.setFocusable(enabled); // mHomeButton.setClickable(enabled); // } // // protected void setProgressBar(int progress) { // mProgressBar.setProgress(progress); // } // // protected void showProgressBar() { // mProgressBar.setVisibility(View.VISIBLE); // } // // protected void hideProgressBar() { // AlphaAnimation animation = new AlphaAnimation(1, 0); // animation.setDuration(350); // animation.setAnimationListener(new AnimationListener() { // @Override // public void onAnimationEnd(Animation animation) { // mProgressBar.setVisibility(View.GONE); // } // // @Override // public void onAnimationRepeat(Animation animation) {} // // @Override // public void onAnimationStart(Animation animation) {} // }); // mProgressBar.startAnimation(animation); // } @Override protected void onPause() { super.onPause(); // Don't do anything if no lock pattern is set if (!mLockPatternUtils.isLockPatternEnabled()) { return; } /* Save the current time If a lock pattern has been set If this activity never loaded set the lock time to 10 seconds ago. This is to prevent the following scenario: 1. Activity Starts 2. Lock screen is displayed 3. User presses the home button 4. "lock time" is set in onPause to when the home button was pressed 5. Activity is started again within 2 seconds and no lock screen is shown this time. */ if (mHasLoaded) { writeLockTime(); } else { writeLockTime(SystemClock.elapsedRealtime() - 10000); } } @Override protected void onResume() { super.onResume(); // Don't do anything if no lock pattern is set if (!mLockPatternUtils.isLockPatternEnabled()) { return; } if (mSkipLockOnce) { mSkipLockOnce = false; return; } // If a lock pattern is set we need to check the time for when the last // activity was open. If it's been more than two seconds the user // will have to enter the lock pattern to continue. long currentTime = SystemClock.elapsedRealtime(); long lockedAt = mPrefs.getLong("locked_at", currentTime - 10000); long timedif = Math.abs(currentTime - lockedAt); if (timedif > 2000) { mHasLoaded = false; launchPatternLock(); } else { mHasLoaded = true; } } private void launchPatternLock() { Intent intent = new Intent(this, ConfirmLockPattern.class); intent.putExtra(ConfirmLockPattern.HEADER_TEXT, getText(R.string.patternlock_header)); startActivityForResult(intent, PATTERNLOCK_UNLOCK); } private void writeLockTime() { writeLockTime(SystemClock.elapsedRealtime()); } private void writeLockTime(long time) { Editor editor = mPrefs.edit(); editor.putLong("locked_at", time); editor.apply(); } protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PATTERNLOCK_UNLOCK) { if (resultCode == RESULT_OK) { writeLockTime(); } else { launchPatternLock(); } } } protected void skipLockOnce() { mSkipLockOnce = true; } @Override public boolean onCreateThumbnail(Bitmap outBitmap, Canvas canvas) { View decorview = getWindow().getDecorView(); if (decorview == null) { return true; } final int vw = decorview.getWidth(); final int vh = decorview.getHeight(); final int dw = outBitmap.getWidth(); final int dh = outBitmap.getHeight(); Bitmap bluredBitmap = Bitmap .createBitmap(outBitmap.getWidth(), outBitmap.getHeight(), outBitmap.getConfig()); Canvas c = new Canvas(bluredBitmap); c.save(); c.scale(((float) dw) / vw, ((float) dh) / vh); decorview.draw(c); c.restore(); canvas.drawBitmap(pixelate(bluredBitmap, 5), 0, 0, null); Bitmap lockbitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lock); Paint p = new Paint(); p.setAntiAlias(true); p.setDither(true); p.setFilterBitmap(true); canvas.drawBitmap(lockbitmap, null, new RectF(dw * 0.25f, dh * 0.25f, dw * 0.75f, dh * 0.75f), p); return true; } private Bitmap pixelate(Bitmap bitmap, int size) { Bitmap bm = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); Canvas c = new Canvas(bm); Paint p = new Paint(); p.setStyle(Style.FILL); int w = bm.getWidth(); int h = bm.getHeight(); int[] pixels = new int[w * h]; bitmap.getPixels(pixels, 0, w, 0, 0, w, h); for (int i = 0; i < h; i = i + size) { for (int j = 0; j < w; j = j + size) { int a = 0; int r = 0; int g = 0; int b = 0; int pc = 0; for (int k = 0; k < size; k++) { for (int l = 0; l < size; l++) { int pxp = (i + k) * w + j + l; if (pxp < pixels.length) { int pixel = pixels[pxp]; a += Color.alpha(pixel); r += Color.red(pixel); g += Color.green(pixel); b += Color.blue(pixel); pc++; } } } a /= pc; r /= pc; g /= pc; b /= pc; p.setColor(Color.argb(a, r, g, b)); c.drawRect(j, i, j + size, i + size, p); } } return bm; } public boolean shouldShowActionBar() { return true; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/LockablePreferenceActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.lockpattern.ConfirmLockPattern; import com.liato.bankdroid.lockpattern.LockPatternUtils; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.view.WindowManager; public class LockablePreferenceActivity extends PreferenceActivity { private static final int PATTERNLOCK_UNLOCK = 42; private SharedPreferences mPrefs; private LockPatternUtils mLockPatternUtils; private boolean mHasLoaded = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mLockPatternUtils = new LockPatternUtils(this); mLockPatternUtils .setVisiblePatternEnabled(mPrefs.getBoolean("patternlock_visible_pattern", true)); mLockPatternUtils.setTactileFeedbackEnabled( mPrefs.getBoolean("patternlock_tactile_feedback", false)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override protected void onPause() { super.onPause(); // Don't do anything if no lock pattern is set if (!mLockPatternUtils.isLockPatternEnabled()) { return; } /* Save the current time If a lock pattern has been set If this activity never loaded set the lock time to 10 seconds ago. This is to prevent the following scenario: 1. Activity Starts 2. Lock screen is displayed 3. User presses the home button 4. "lock time" is set in onPause to when the home button was pressed 5. Activity is started again within 2 seconds and no lock screen is shown this time. */ if (mHasLoaded) { writeLockTime(); } else { writeLockTime(SystemClock.elapsedRealtime() - 10000); } } @Override protected void onResume() { super.onResume(); // Don't do anything if lock pattern is not set if (!mLockPatternUtils.isLockPatternEnabled() || !isLockEnabled()) { return; } // If a lock pattern is set we need to check the time for when the last // activity was open. If it's been more than two seconds the user // will have to enter the lock pattern to continue. long currentTime = SystemClock.elapsedRealtime(); long lockedAt = mPrefs.getLong("locked_at", currentTime - 10000); long timedif = Math.abs(currentTime - lockedAt); if (timedif > 2000) { launchPatternLock(); } else { mHasLoaded = true; } } private void launchPatternLock() { Intent intent = new Intent(this, ConfirmLockPattern.class); intent.putExtra(ConfirmLockPattern.HEADER_TEXT, getText(R.string.patternlock_header)); startActivityForResult(intent, PATTERNLOCK_UNLOCK); } private void writeLockTime() { writeLockTime(SystemClock.elapsedRealtime()); } private void writeLockTime(long time) { Editor editor = mPrefs.edit(); editor.putLong("locked_at", time); editor.apply(); } protected boolean isLockEnabled() { return mPrefs.getBoolean("lock_enabled", true); } protected void setLockEnabled(boolean enabled) { Editor editor = mPrefs.edit(); editor.putBoolean("lock_enabled", enabled); editor.apply(); } protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PATTERNLOCK_UNLOCK) { if (resultCode == RESULT_OK) { writeLockTime(); } else { launchPatternLock(); } } } @Override protected void onStop() { super.onStop(); setLockEnabled(true); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/MainActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.adapters.AccountsAdapter; import com.liato.bankdroid.appwidget.AutoRefreshService; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.db.DBAdapter; import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; import butterknife.ButterKnife; import butterknife.InjectView; public class MainActivity extends LockableActivity { @InjectView(R.id.txtAccountsDesc) TextView mAccountsDescription; protected static boolean showHidden = false; private Bank selectedBank = null; private static Account selectedAccount = null; private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { refreshView(); } }; protected AccountsAdapter adapter = null; @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); PairApplicationsActivity.initialSetupApiKey(this); setContentView(R.layout.main); ButterKnife.inject(this); adapter = new AccountsAdapter(this, showHidden); final ArrayList banks = new ArrayList<>(); adapter.setGroups(banks); final ListView lv = (ListView) findViewById(R.id.lstAccountsList); lv.setAdapter(adapter); lv.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(final AdapterView parent, final View view, final int position, final long id) { if (adapter.getItem(position) instanceof Account) { selectedAccount = (Account) adapter.getItem(position); final PopupMenuAccount pmenu = new PopupMenuAccount(parent, view, MainActivity.this); pmenu.showLikeQuickAction(0, 12); return true; } else if (adapter.getItem(position) instanceof Bank) { selectedBank = (Bank) adapter.getItem(position); selectedBank.toggleHideAccounts(); DBAdapter.save(selectedBank, MainActivity.this); refreshView(); return true; } return false; } }); lv.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { if (adapter.getItem(position) instanceof Bank) { selectedBank = (Bank) adapter.getItem(position); final PopupMenuBank pmenu = new PopupMenuBank(parent, view, MainActivity.this); pmenu.showLikeQuickAction(0, 12); } else { final Intent intent = new Intent(MainActivity.this, TransactionsActivity.class); final Account account = (Account) adapter.getItem(position); intent.putExtra("account", account.getId()); intent.putExtra("bank", account.getBankDbId()); MainActivity.this.startActivity(intent); } } }); final Bundle extras = getIntent().getExtras(); // Clicking on widgets opens the transactions history through MainActivity so that // the user can back out to the main window. if (AutoRefreshService.ACTION_MAIN_SHOW_TRANSACTIONS.equals(getIntent().getAction())) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (prefs.getBoolean("widget_opens_transactions", true)) { skipLockOnce(); final Intent intent = new Intent(this, TransactionsActivity.class); intent.putExtra("account", extras.getString("account")); intent.putExtra("bank", extras.getLong("bank")); startActivity(intent); } } ((BankdroidApplication) getApplication()).showAndDeleteApplicationMessage(); } @Override public void onResume() { super.onResume(); // Receive refresh Intent from AutoRefreshService and refresh the main view if changes // have been detected. registerReceiver(receiver, new IntentFilter(AutoRefreshService.BROADCAST_MAIN_REFRESH)); refreshView(); } public void refreshView() { final ArrayList banks = BankFactory.banksFromDb(this, true); mAccountsDescription.setVisibility(banks.isEmpty() ? View.VISIBLE : View.GONE); adapter.setShowHidden(showHidden); adapter.setGroups(banks); adapter.notifyDataSetChanged(); } @Override public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); final MenuInflater inflater = new MenuInflater(this); inflater.inflate(R.menu.main, menu); return true; } @Override protected Dialog onCreateDialog(final int id) { super.onCreateDialog(id); final Dialog dialog = new Dialog(this); dialog.setContentView(R.layout.about); dialog.setTitle(getString(R.string.about)); String version = BuildConfig.VERSION_NAME; ((TextView) dialog.findViewById(R.id.txtVersion)) .setText(getText(R.string.version).toString().replace("$version", version)); return dialog; } @Override public boolean onOptionsItemSelected(final MenuItem item) { Intent intent; switch (item.getItemId()) { case R.id.action_toggle_hidden: showHidden = !showHidden; item.setTitle(showHidden ? R.string.menu_hide_hidden : R.string.menu_show_hidden); refreshView(); return true; case R.id.action_settings: intent = new Intent(this, SettingsActivity.class); this.startActivity(intent); //Helpers.setActivityAnimation(this, R.anim.zoom_enter, R.anim.zoom_exit); return true; case R.id.action_about: intent = new Intent(this, AboutActivity.class); startActivity(intent); return true; case R.id.action_refresh: new DataRetrieverTask(MainActivity.this).execute(); return true; case R.id.action_add: final Intent intentAccount = new Intent(MainActivity.this, BankEditActivity.class); startActivity(intentAccount); return true; } return false; } @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(receiver); } /** * Extends {@link BetterPopupWindow} *

* Overrides onCreate to create the view and register the button listeners * * @author qbert */ private static class PopupMenuBank extends BetterPopupWindow implements OnClickListener { MainActivity parent = null; public PopupMenuBank(final ViewGroup parentView, final View anchor, final MainActivity parent) { super(parentView, anchor); this.parent = parent; } @Override protected void onCreate() { // inflate layout final LayoutInflater inflater = (LayoutInflater) this.anchor.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.popup_bank, this.parentView, false); final Button btnHide = (Button) root.findViewById(R.id.btnHide); final Button btnUnhide = (Button) root.findViewById(R.id.btnUnhide); final Button btnWWW = (Button) root.findViewById(R.id.btnWWW); if (parent.selectedBank.getHideAccounts()) { btnHide.setVisibility(View.GONE); btnUnhide.setVisibility(View.VISIBLE); btnUnhide.setOnClickListener(this); } else { btnHide.setVisibility(View.VISIBLE); btnUnhide.setVisibility(View.GONE); btnHide.setOnClickListener(this); } if (parent.selectedBank.isWebViewEnabled()) { btnWWW.setOnClickListener(this); } else { btnWWW.setVisibility(View.GONE); } root.findViewById(R.id.btnRefresh).setOnClickListener(this); root.findViewById(R.id.btnEdit).setOnClickListener(this); root.findViewById(R.id.btnRemove).setOnClickListener(this); this.setContentView(root); } @Override public void onClick(final View v) { final Context context = this.anchor.getContext(); final int id = v.getId(); switch (id) { case R.id.btnHide: case R.id.btnUnhide: this.dismiss(); parent.selectedBank.toggleHideAccounts(); DBAdapter.save(parent.selectedBank, context); parent.refreshView(); return; case R.id.btnWWW: if (parent.selectedBank != null && parent.selectedBank.isWebViewEnabled()) { //Uri uri = Uri.parse(selectedBank.getURL()); //Intent intent = new Intent(Intent.ACTION_VIEW, uri); final Intent intent = new Intent(context, WebViewActivity.class); intent.putExtra("bankid", parent.selectedBank.getDbId()); context.startActivity(intent); } this.dismiss(); return; case R.id.btnEdit: final Intent intent = new Intent(context, BankEditActivity.class); intent.putExtra("id", parent.selectedBank.getDbId()); context.startActivity(intent); this.dismiss(); return; case R.id.btnRefresh: this.dismiss(); new DataRetrieverTask(parent, parent.selectedBank.getDbId()).execute(); return; case R.id.btnRemove: this.dismiss(); final AlertDialog.Builder builder = new AlertDialog.Builder(context); //builder.setMessage(getText(R.string.passwords_mismatch)).setTitle(getText(R.string.passwords_mismatch_title)) builder.setMessage(context.getText(R.string.remove_bank_msg)) .setTitle(context.getText(R.string.remove_bank_title)) .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(context.getText(R.string.yes), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int id) { final DBAdapter db = new DBAdapter(context); db.deleteBank(parent.selectedBank.getDbId()); dialog.cancel(); parent.refreshView(); } }) .setNegativeButton(context.getText(R.string.no), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int id) { dialog.cancel(); } }); final AlertDialog alert = builder.create(); alert.show(); return; } } } /** * Extends {@link BetterPopupWindow} *

* Overrides onCreate to create the view and register the button listeners * * @author qbert */ private static class PopupMenuAccount extends BetterPopupWindow implements OnClickListener { MainActivity parent = null; public PopupMenuAccount(final ViewGroup parentView, final View anchor, final MainActivity parent) { super(parentView, anchor); this.parent = parent; } @Override protected void onCreate() { final LayoutInflater inflater = (LayoutInflater) this.anchor.getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.popup_account, this.parentView, false); final Button btnHide = (Button) root.findViewById(R.id.btnHide); final Button btnUnhide = (Button) root.findViewById(R.id.btnUnhide); final Button btnDisableNotifications = (Button) root .findViewById(R.id.btnDisableNotifications); final Button btnEnableNotifications = (Button) root .findViewById(R.id.btnEnableNotifications); if (selectedAccount.isHidden()) { btnHide.setVisibility(View.GONE); btnUnhide.setVisibility(View.VISIBLE); btnUnhide.setOnClickListener(this); } else { btnHide.setVisibility(View.VISIBLE); btnUnhide.setVisibility(View.GONE); btnHide.setOnClickListener(this); } if (selectedAccount.isNotify()) { btnDisableNotifications.setVisibility(View.VISIBLE); btnDisableNotifications.setOnClickListener(this); btnEnableNotifications.setVisibility(View.GONE); } else { btnDisableNotifications.setVisibility(View.GONE); btnEnableNotifications.setOnClickListener(this); btnEnableNotifications.setVisibility(View.VISIBLE); } this.setContentView(root); } @Override public void onClick(final View v) { final int id = v.getId(); switch (id) { case R.id.btnHide: this.dismiss(); selectedAccount.setHidden(true); DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnUnhide: this.dismiss(); selectedAccount.setHidden(false); DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnEnableNotifications: this.dismiss(); selectedAccount.setNotify(true); DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnDisableNotifications: this.dismiss(); selectedAccount.setNotify(false); DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; } } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/PairApplicationsActivity.java ================================================ /** * Copyright 2011 Magnus Andersson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.provider.BankTransactionsProvider; import com.liato.bankdroid.provider.IBankTransactionsProvider; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.KeyEvent; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import timber.log.Timber; /** * @author Magnus Andersson * @since 8 jun 2011 */ public class PairApplicationsActivity extends LockableActivity { private static final String PAIR_APP_NAME = "com.liato.bankdroid.PAIR_APP_NAME"; public static void initialSetupApiKey(Context ctx) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); final String apiKey = prefs.getString("content_provider_api_key", ""); if (apiKey.equals("")) { final SharedPreferences.Editor editor = prefs.edit(); // Create a random HEX String final String genKey = Long.toHexString(Double.doubleToLongBits(Math.random())); // Commit to preferences editor.putString("content_provider_api_key", genKey.toUpperCase()); editor.apply(); } } /** {@inheritDoc} */ @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.pair_applications_layout); // setHomeButtonEnabled(false); Bundle bundle = getIntent().getExtras(); if (bundle.containsKey(PAIR_APP_NAME)) { String appName = bundle.getString(PAIR_APP_NAME); ImageView img = (ImageView) findViewById(R.id.imageView1); // Note that we used to load this dynamically, but Ekonomipuls was the // only user ever. Doing it statically like this helps Android Lint // know that the logo in question is still in use. Drawable d = getResources().getDrawable(R.drawable.applogo_ekonomipuls); img.setImageDrawable(d); img.requestLayout(); // Change application name TextView appNameView = (TextView) findViewById(R.id.app_name); appNameView.setText(appName); } else { Timber.w("Unknown application"); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { //Handle the back button if (keyCode == KeyEvent.KEYCODE_BACK) { setResult(RESULT_CANCELED); finish(); return true; } return super.onKeyDown(keyCode, event); } public void cancelPairing(final View v) { setResult(RESULT_CANCELED); finish(); } public void confirmPairing(final View v) { Intent intent = this.getIntent(); Context ctx = getBaseContext(); // Make sure sharing is enabled SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx); Editor editor = pref.edit(); editor.putBoolean("content_provider_enabled", true); editor.apply(); String apiKey; try { apiKey = BankTransactionsProvider.getApiKey(ctx); } catch (IllegalArgumentException e) { //Initialize API key if it is not set initialSetupApiKey(ctx); apiKey = BankTransactionsProvider.getApiKey(ctx); } intent.putExtra(IBankTransactionsProvider.API_KEY, apiKey); setResult(RESULT_OK, intent); finish(); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/SettingsActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.appwidget.AutoRefreshService; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.banks.TestBank; import com.liato.bankdroid.banking.banks.nordea.Nordea; import com.liato.bankdroid.lockpattern.ChooseLockPattern; import com.liato.bankdroid.lockpattern.ConfirmLockPattern; import com.liato.bankdroid.lockpattern.LockPatternUtils; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; import android.widget.Toast; import java.math.BigDecimal; import timber.log.Timber; public class SettingsActivity extends LockablePreferenceActivity implements OnPreferenceClickListener, OnPreferenceChangeListener { private final static int DISABLE_LOCKPATTERN = 1; private final static int ENABLE_LOCKPATTERN = 2; private final static int CHANGE_LOCKPATTERN = 3; private LockPatternUtils mLockPatternUtils; /** Called when the activity is first created. */ @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mLockPatternUtils = new LockPatternUtils(this); addPreferencesFromResource(R.xml.settings); getWindow().setBackgroundDrawableResource(R.drawable.background_repeat); (findPreference("patternlock_change")).setOnPreferenceClickListener(this); (findPreference("remotenotifier_help")).setOnPreferenceClickListener(this); (findPreference("openwatch_help")).setOnPreferenceClickListener(this); (findPreference("liveview_help")).setOnPreferenceClickListener(this); (findPreference("account_types_screen")).setOnPreferenceClickListener(this); (findPreference("remotenotifier_screen")).setOnPreferenceClickListener(this); (findPreference("openwatch_screen")).setOnPreferenceClickListener(this); (findPreference("liveview_screen")).setOnPreferenceClickListener(this); (findPreference("notification_sound")).setOnPreferenceClickListener(this); (findPreference("test_notification")).setOnPreferenceClickListener(this); (findPreference("notify_min_delta")).setOnPreferenceChangeListener(this); final CheckBoxPreference patternLock = ((CheckBoxPreference) findPreference( "patternlock_enabled")); patternLock.setOnPreferenceClickListener(this); // Check the pattern lock check box if the lock pattern is enabled patternLock.setChecked(mLockPatternUtils.isLockPatternEnabled()); } @Override public boolean onPreferenceClick(final Preference pref) { final String prefKey = pref.getKey(); if ("account_types_screen".equals(prefKey) || "remotenotifier_screen".equals(prefKey) || "openwatch_screen".equals(prefKey) || "liveview_screen".equals(prefKey)) { ((PreferenceScreen) pref).getDialog().getWindow() .setBackgroundDrawableResource(R.drawable.background_repeat); return false; } if ("notification_sound".equals(prefKey)) { return false; } if ("patternlock_enabled".equals(prefKey)) { this.setLockEnabled(false); if (mLockPatternUtils.isLockPatternEnabled()) { // The user is trying to disable the lock pattern, // only disable if the user knows the pattern. startActivityForResult(new Intent(this, ConfirmLockPattern.class), DISABLE_LOCKPATTERN); return true; } else { // No lock pattern has been set yet, let the user choose a new one. startActivityForResult(new Intent(this, ChooseLockPattern.class), ENABLE_LOCKPATTERN); return true; } } else if ("patternlock_change".equals(prefKey)) { this.setLockEnabled(false); startActivityForResult(new Intent(this, ChooseLockPattern.class), CHANGE_LOCKPATTERN); return true; } else if ("remotenotifier_help".equals(prefKey)) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://code.google.com/p/android-notifier/"))); return true; } else if ("openwatch_help".equals(prefKey)) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://forum.xda-developers.com/showthread.php?t=554551"))); return true; } else if ("liveview_help".equals(prefKey)) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse( "http://www.sonyericsson.com/cws/products/accessories/overview/liveviewmicrodisplay"))); return true; } else if ("test_notification".equals(prefKey)) { Timber.d("Sending test notification."); Account account1 = new Account("Personkonto", new BigDecimal(8351.00), "22"); Bank bank1 = new TestBank(this); bank1.setDbid(21); bank1.setCustomName("800416-0001"); Account account2 = new Account("Personkonto", new BigDecimal(8341.00), "23"); Bank bank2 = new Nordea(this); bank2.setDbid(22); bank2.setCustomName("800416-0002"); AutoRefreshService.showNotification(bank1, account1, new BigDecimal(-143.50), this); AutoRefreshService.showNotification(bank2, account2, new BigDecimal(-123.50), this); return true; } return false; } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == DISABLE_LOCKPATTERN) { if (resultCode == RESULT_OK) { mLockPatternUtils.setLockPatternEnabled(false); mLockPatternUtils.saveLockPattern(null); ((CheckBoxPreference) findPreference("patternlock_enabled")).setChecked(false); } else { Timber.d("User was unable to disable pattern lock."); } } else if (requestCode == ENABLE_LOCKPATTERN) { // User attempted to enable the pattern lock, toggle the checkbox. ((CheckBoxPreference) findPreference("patternlock_enabled")) .setChecked(mLockPatternUtils.isLockPatternEnabled()); } else if (requestCode == CHANGE_LOCKPATTERN) { // Don't do anything special } } @Override protected void onResume() { super.onResume(); this.setLockEnabled(true); } @Override protected void onPause() { super.onPause(); StartupReceiver.setAlarm(this); // Blur/unblur the widget balance AutoRefreshService.sendWidgetRefresh(this); } @Override public boolean onPreferenceChange(Preference pref, Object newValue) { final String prefKey = pref.getKey(); if ("notify_min_delta".equals(prefKey)) { Integer val; try { val = Integer.valueOf((String) newValue); } catch (NumberFormatException e) { val = null; } if (val != null && val >= 0) { return true; } else { Toast.makeText(pref.getContext(), String.format(pref.getContext().getString(R.string.invalid_integer), newValue), Toast.LENGTH_LONG).show(); } return false; } return false; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/StartupReceiver.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.appwidget.AutoRefreshService; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.os.SystemClock; import android.preference.PreferenceManager; public class StartupReceiver extends BroadcastReceiver { public static void setAlarm(Context context) { PendingIntent alarmSender; alarmSender = PendingIntent .getService(context, 0, new Intent(context, AutoRefreshService.class), PendingIntent.FLAG_UPDATE_CURRENT); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean autoUpdatesEnabled = prefs.getBoolean("autoupdates_enabled", true); Integer refreshRate = Integer.parseInt(prefs.getString("refresh_rate", "0")); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (!autoUpdatesEnabled) { am.cancel(alarmSender); } else { long firstTime = SystemClock.elapsedRealtime(); int secondsInMinute = 60; if (prefs.getBoolean("debug_mode", false) && prefs .getBoolean("debug_refreshrate_in_seconds", false)) { secondsInMinute = 1; } am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime + refreshRate * secondsInMinute * 1000, refreshRate * secondsInMinute * 1000, alarmSender); } } public static void updateNow(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean autoUpdatesEnabled = prefs.getBoolean("autoupdates_enabled", true); long lastUpdate = prefs.getLong("autoupdates_last_update", 0); Integer refreshRate = Integer.parseInt(prefs.getString("refresh_rate", "0")); if (autoUpdatesEnabled && System.currentTimeMillis() - lastUpdate > refreshRate * 60 * 1000) { context.startService(new Intent(context, AutoRefreshService.class)); } } @Override public void onReceive(Context context, Intent intent) { //Set alarms for auto updates on boot, package update, package replace and package new if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { updateNow(context); } else { setAlarm(context); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/TimePreference.java ================================================ //This class based on tutorial found here https://github.com/commonsguy/cw-lunchlist/blob/master/19-Alarm/LunchList/src/apt/tutorial/TimePreference.java package com.liato.bankdroid; import android.content.Context; import android.content.res.TypedArray; import android.preference.DialogPreference; import android.util.AttributeSet; import android.view.View; import android.widget.TimePicker; import timber.log.Timber; public class TimePreference extends DialogPreference { private int lastValue = 0; private TimePicker picker = null; public TimePreference(Context ctxt, AttributeSet attrs) { super(ctxt, attrs); setPositiveButtonText("Set"); setNegativeButtonText("Cancel"); } @Override protected View onCreateDialogView() { picker = new TimePicker(getContext()); picker.setIs24HourView(true); return picker; } @Override protected void onBindDialogView(View v) { super.onBindDialogView(v); picker.setCurrentHour(lastValue / 60); picker.setCurrentMinute(lastValue % 60); } @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); if (positiveResult) { lastValue = picker.getCurrentHour() * 60 + picker.getCurrentMinute(); if (callChangeListener(lastValue)) { persistInt(lastValue); } } } @Override protected Object onGetDefaultValue(TypedArray a, int index) { return (a.getInt(index, 0)); } @Override protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { int val = 0; if (restoreValue) { val = getPersistedInt(val); } else { try { val = Integer.parseInt(defaultValue.toString()); } catch (NumberFormatException e) { Timber.e(e, "TimePreference's defaultValue is not a number"); } } lastValue = val; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/TransactionsActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.banking.Transaction; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class TransactionsActivity extends LockableActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.transactions); Bundle extras = getIntent().getExtras(); Bank bank = BankFactory.bankFromDb(extras.getLong("bank"), this, false); if (bank == null) { redirectToMain(getString(R.string.error_bank_not_found)); return; } Account account = BankFactory .accountFromDb(this, extras.getLong("bank") + "_" + extras.getString("account"), true); if (account == null) { redirectToMain(getString(R.string.error_account_not_found)); return; } TextView viewBankName = (TextView) findViewById(R.id.txtListitemAccountsGroupAccountname); TextView viewAccountName = (TextView) findViewById(R.id.txtListitemAccountsGroupBankname); TextView viewAccountBalance = (TextView) findViewById(R.id.txtListitemAccountsGroupTotal); ImageView icon = (ImageView) findViewById(R.id.imgListitemAccountsGroup); viewBankName.setText(bank.getDisplayName()); viewAccountName.setText(account.getName()); viewAccountBalance .setText(Helpers.formatBalance(account.getBalance(), account.getCurrency())); icon.setImageResource(bank.getImageResource()); List transactions = account.getTransactions(); if (bank.isDisabled()) { findViewById(R.id.txtDisabledWarning).setVisibility(View.VISIBLE); } if (!transactions.isEmpty()) { Collections.sort(transactions); findViewById(R.id.txtTranDesc).setVisibility(View.GONE); TransactionsAdapter adapter = new TransactionsAdapter(transactions); ListView viewTransactionsList = (ListView) findViewById(R.id.lstTransactionsList); viewTransactionsList.setAdapter(adapter); } findViewById(R.id.layBankHeader).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); } @Override protected void onResume() { super.onResume(); } private void redirectToMain(String errorMessage) { final Intent intent = new Intent(this, MainActivity.class); ((BankdroidApplication) getApplicationContext()) .setApplicationMessage(errorMessage); startActivity(intent); } private class TransactionsAdapter extends BaseAdapter { private LayoutInflater inflater; private ArrayList items = new ArrayList<>(); public TransactionsAdapter(List transactions) { inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (!transactions.isEmpty()) { String date = transactions.get(0).getDate(); items.add(date); for (Transaction transaction : transactions) { if (!date.equals(transaction.getDate())) { date = transaction.getDate(); items.add(date); } items.add(transaction); } } } public View newTransactionView(Transaction transaction, ViewGroup parent, View convertView) { if (convertView == null) { convertView = inflater.inflate(R.layout.transaction_item, parent, false); } ((TextView) convertView.findViewById(R.id.txtTransaction)) .setText(transaction.getTransaction()); ((TextView) convertView.findViewById(R.id.txtAmount)).setText( Helpers.formatBalance(transaction.getAmount(), transaction.getCurrency())); if (transaction.getAmount().signum() == 1) { convertView.findViewById(R.id.imgColor) .setBackgroundResource(R.drawable.transaction_positive); } else { convertView.findViewById(R.id.imgColor) .setBackgroundResource(R.drawable.transaction_negative); } return convertView; } public View newDateView(String date, ViewGroup parent, View convertView) { if (convertView == null) { convertView = inflater.inflate(R.layout.transaction_date, parent, false); } ((TextView) convertView.findViewById(R.id.txtDate)).setText(date); return convertView; } @Override public int getCount() { return items.size(); } @Override public Object getItem(int position) { return items.get(position); } @Override public long getItemId(int position) { return position; } @Override @Nullable public View getView(int position, View convertView, ViewGroup parent) { Object item = getItem(position); if (item == null) { return null; } if (item instanceof Transaction) { return newTransactionView((Transaction) item, parent, convertView); } else if (item instanceof String) { return newDateView((String) item, parent, convertView); } return null; } @Override public boolean areAllItemsEnabled() { return true; } @Override public boolean isEnabled(int position) { return true; } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { Object item = getItem(position); if (item instanceof Transaction) { return 0; } return 1; } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/WebViewActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.Bank.SessionPackage; import com.liato.bankdroid.banking.BankFactory; import org.apache.commons.io.IOUtils; import org.apache.http.client.CookieStore; import org.apache.http.cookie.Cookie; import android.content.res.Resources.NotFoundException; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; import java.io.IOException; import eu.nullbyte.android.urllib.Urllib; import timber.log.Timber; import static android.graphics.Color.WHITE; public class WebViewActivity extends LockableActivity implements OnClickListener { private WebView mWebView; private boolean mFirstPageLoaded = false; private Handler mMainThreadhandler = new Handler(Looper.getMainLooper()); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.webview); //TODO // this.addTitleButton(R.drawable.title_icon_back, "back", this); // this.addTitleButton(R.drawable.title_icon_forward, "forward", this); // this.addTitleButton(R.drawable.title_icon_refresh, "refresh", this); // this.setTitleButtonEnabled("forward", false); // this.setTitleButtonEnabled("back", false); // this.setTitleButtonEnabled("refresh", false); final CookieSyncManager csm = CookieSyncManager.createInstance(this); mWebView = (WebView) findViewById(R.id.wvBank); mWebView.setBackgroundColor(0); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.getSettings().setBuiltInZoomControls(true); mWebView.getSettings().setUserAgentString(Urllib.DEFAULT_USER_AGENT); mWebView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); mWebView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView view, int progress) { // activity.setProgressBar(progress); if (progress == 100) { Handler handler = new Handler(); Runnable runnable = new Runnable() { public void run() { // activity.hideProgressBar(); if (mFirstPageLoaded) { mWebView.setBackgroundColor(WHITE); } } }; // Let the progress bar hit 100% before we hide it. handler.postDelayed(runnable, 100); } else if (mFirstPageLoaded) { // activity.showProgressBar(); } } }); mWebView.setWebViewClient(new BankWebViewClient()); String preloader = "Error..."; try { preloader = IOUtils.toString(getResources().openRawResource(R.raw.loading)); preloader = String.format(preloader, "", // Javascript function "" // HTML ); } catch (NotFoundException | IOException e) { Timber.w(e, "Error loading loading.html"); } mWebView.loadDataWithBaseURL("what://is/this/i/dont/even", preloader, "text/html", "utf-8", null); Bundle extras = getIntent().getExtras(); final long bankId = extras.getLong("bankid", -1); //final long bankId = -1; if (bankId >= 0) { Runnable generateLoginPage = new Runnable() { public void run() { Bank bank = BankFactory.bankFromDb(bankId, WebViewActivity.this, false); final SessionPackage loginPackage = bank .getSessionPackage(WebViewActivity.this); final CookieStore cookieStore = loginPackage.getCookiestore(); mMainThreadhandler.post(new Runnable() { @Override public void run() { if ((cookieStore != null) && !cookieStore.getCookies().isEmpty()) { CookieManager cookieManager = CookieManager.getInstance(); CookieManager.getInstance().removeSessionCookie(); String cookieString; for (Cookie cookie : cookieStore.getCookies()) { cookieString = String.format("%s=%s;%spath=%s; domain=%s;", cookie.getName(), cookie.getValue(), cookie.getExpiryDate() == null ? "" : "expires=" + cookie.getExpiryDate() + "; ", cookie.getPath() == null ? "/" : cookie.getPath(), cookie.getDomain()); cookieManager.setCookie(cookie.getDomain(), cookieString); } csm.sync(); } mWebView.loadDataWithBaseURL("what://is/this/i/dont/even", loginPackage.getHtml(), "text/html", "utf-8", null); } }); } }; new Thread(generateLoginPage).start(); } } public void onResume() { super.onResume(); } //Handle the back key @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mWebView != null) { if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) { mWebView.goBack(); return true; } } return super.onKeyDown(keyCode, event); } @Override public void onClick(View v) { String tag = (String) v.getTag(); if ("refresh".equals(tag)) { mWebView.reload(); } else if ("back".equals(tag)) { mWebView.goBack(); } else if ("forward".equals(tag)) { mWebView.goForward(); } } // Make sure clicked links are loaded in our webview. private class BankWebViewClient extends WebViewClient { @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); if (mFirstPageLoaded) { handleHistoryChange(); } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (!mFirstPageLoaded) { //This is the generated POST page. if (url.startsWith("what:")) { return; } //This is the first real page. //Remove the generated page from history. mWebView.clearHistory(); mFirstPageLoaded = true; // activity.setTitleButtonEnabled("refresh", true); return; } } @Override public void onFormResubmission(WebView view, Message dontResend, Message resend) { // TODO Auto-generated method stub //super.onFormResubmission(view, dontResend, resend); resend.sendToTarget(); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } public void handleHistoryChange() { // activity.setTitleButtonEnabled("back", mWebView.canGoBack()); // activity.setTitleButtonEnabled("forward", mWebView.canGoForward()); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/adapters/AccountsAdapter.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.adapters; import com.liato.bankdroid.Helpers; import com.liato.bankdroid.R; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; public class AccountsAdapter extends BaseAdapter { public final static int VIEWTYPE_BANK = 0; public final static int VIEWTYPE_ACCOUNT = 1; public final static int VIEWTYPE_EMPTY = 2; private final SharedPreferences prefs; private ArrayList banks; private final LayoutInflater inflater; private boolean showHidden; public AccountsAdapter(Context context, boolean showHidden) { this.banks = new ArrayList<>(); inflater = LayoutInflater.from(context); this.showHidden = showHidden; prefs = PreferenceManager.getDefaultSharedPreferences(context); } public void setGroups(ArrayList banks) { this.banks = banks; } public void setShowHidden(boolean showHidden) { this.showHidden = showHidden; } private View newBankView(Bank bank, ViewGroup parent, View convertView) { if (convertView == null) { convertView = inflater.inflate(R.layout.listitem_accounts_group, parent, false); } ImageView icon = (ImageView) convertView.findViewById(R.id.imgListitemAccountsGroup); ((TextView) convertView.findViewById(R.id.txtListitemAccountsGroupAccountname)) .setText(bank.getDisplayName()); ((TextView) convertView.findViewById(R.id.txtListitemAccountsGroupBankname)) .setText(bank.getName()); ((TextView) convertView .findViewById(R.id.txtListitemAccountsGroupTotal)) .setText(Helpers.formatBalance(bank.getBalance(), bank.getCurrency(), prefs.getBoolean("round_balance", false) || !bank.getDisplayDecimals(), bank.getDecimalFormatter(), false)); icon.setImageResource(bank.getImageResource()); View warning = convertView.findViewById(R.id.txtDisabledWarningX); if (bank.isDisabled()) { warning.setVisibility(View.VISIBLE); } else { warning.setVisibility(View.GONE); } return convertView; } private View newAccountView(Account account, ViewGroup parent, View convertView) { if ((account.isHidden() && !showHidden) || account.getBank().getHideAccounts()) { return convertView == null ? inflater.inflate(R.layout.empty, parent, false) : convertView; } if (convertView == null) { convertView = inflater.inflate(R.layout.listitem_accounts_item, parent, false); } convertView.findViewById(R.id.divider).setBackgroundColor(Color.argb(30, 255, 255, 255)); TextView txtAccountName = ((TextView) convertView .findViewById(R.id.txtListitemAccountsItemAccountname)); TextView txtBalance = ((TextView) convertView .findViewById(R.id.txtListitemAccountsItemBalance)); txtAccountName.setText(account.getName()); txtBalance.setText(Helpers.formatBalance(account.getBalance(), account.getCurrency())); txtBalance .setText(Helpers.formatBalance(account.getBalance(), account.getCurrency(), prefs.getBoolean("round_balance", false) || !account.getBank() .getDisplayDecimals(), account.getBank().getDecimalFormatter(), false)); if (account.isHidden()) { txtAccountName.setTextColor(Color.argb(255, 191, 191, 191)); txtBalance.setTextColor(Color.argb(255, 191, 191, 191)); } else { txtAccountName.setTextColor(Color.WHITE); txtBalance.setTextColor(Color.WHITE); } return convertView; } @Override public int getCount() { int c = 0; for (Bank g : banks) { if (g.getHideAccounts()) { c++; } else { c += g.getAccounts().size() + 1; } } return c; } @Override @Nullable public Object getItem(int position) { if (banks.size() == 0) { return null; } if (position == 0) { return banks.get(0); } int i = 0; for (Bank g : banks) { if (position == i) { return g; } else if (g.getHideAccounts()) { i++; continue; } else if (position <= (g.getAccounts().size() + i)) { return g.getAccounts().get(position - i - 1); } i += g.getAccounts().size() + 1; } return (null); } @Override public long getItemId(int position) { return position; } @Override @Nullable public View getView(int position, View convertView, ViewGroup parent) { Object item = getItem(position); if (item == null) { return null; } if (item instanceof Bank) { return newBankView((Bank)item, parent, convertView); } else if (item instanceof Account) { return newAccountView((Account)item, parent, convertView); } return null; } @Override public boolean isEnabled(int position) { if (getItemViewType(position) == VIEWTYPE_EMPTY) { return false; } return true; } @Override public int getViewTypeCount() { return 3; } @Override public int getItemViewType(int position) { Object item = getItem(position); if (item instanceof Bank) { return VIEWTYPE_BANK; } else { final Account account = (Account)item; if ((account.isHidden() && !showHidden) || account.getBank().getHideAccounts()) { return VIEWTYPE_EMPTY; } } return VIEWTYPE_ACCOUNT; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.appwidget; import com.liato.bankdroid.Helpers; import com.liato.bankdroid.MainActivity; import com.liato.bankdroid.R; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.liveview.LiveViewService; import com.liato.bankdroid.utils.LoggingUtils; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.IBinder; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import timber.log.Timber; public class AutoRefreshService extends Service { public final static String BROADCAST_WIDGET_REFRESH = "com.liato.bankdroid.WIDGET_REFRESH"; public final static String BROADCAST_MAIN_REFRESH = "com.liato.bankdroid.MAIN_REFRESH"; public final static String BROADCAST_REMOTE_NOTIFIER = "org.damazio.notifier.service.UserReceiver.USER_MESSAGE"; public final static String BROADCAST_OPENWATCH_TEXT = "com.smartmadsoft.openwatch.action.TEXT"; public final static String BROADCAST_OPENWATCH_VIBRATE = "com.smartmadsoft.openwatch.action.VIBRATE"; public final static String ACTION_MAIN_SHOW_TRANSACTIONS = "com.liato.bankdroid.action.MAIN_SHOW_TRANSACTIONS"; public final static String BROADCAST_TRANSACTIONS_UPDATED = "com.liato.bankdroid.action.TRANSACTIONS"; public static void showNotification(final Bank bank, final Account account, final BigDecimal diff, Context context) { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); if (!prefs.getBoolean("notify_on_change", true)) { return; } String text = String.format("%s: %s%s", account.getName(), ((diff.compareTo(new BigDecimal(0)) == 1) ? "+" : ""), Helpers.formatBalance(diff, account.getCurrency())); if (!prefs.getBoolean("notify_delta_only", false)) { text = String.format("%s (%s)", text, Helpers.formatBalance(account.getBalance(), account.getCurrency())); } final NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); final NotificationCompat.Builder notification = new NotificationCompat.Builder(context) .setSmallIcon(bank.getImageResource()) .setContentTitle(bank.getDisplayName()) .setContentText(text); // Remove notification from statusbar when clicked notification.setAutoCancel(true); // http://www.freesound.org/samplesViewSingle.php?id=75235 // http://www.freesound.org/samplesViewSingle.php?id=91924 if (prefs.getString("notification_sound", null) != null) { notification.setSound(Uri.parse(prefs.getString( "notification_sound", null))); } if (prefs.getBoolean("notify_with_vibration", true)) { final long[] vib = {0, 90, 130, 80, 350, 190, 20, 380}; notification.setVibrate(vib); } if (prefs.getBoolean("notify_with_led", true)) { notification.setLights(prefs.getInt("notify_with_led_color", context.getResources().getColor(R.color.default_led_color)), 700, 200); } final PendingIntent contentIntent = PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); notification.setContentIntent(contentIntent); String numNotifications = prefs.getString("num_notifications", "total"); int notificationId = (int) (numNotifications.equals("total") ? 0 : numNotifications.equals("bank") ? bank.getDbId() : numNotifications.equals("account") ? account.getId().hashCode() : SystemClock.elapsedRealtime()); notificationManager.notify(notificationId, notification.build()); // Broadcast to Remote Notifier if enabled // http://code.google.com/p/android-notifier/ if (prefs.getBoolean("notify_remotenotifier", false)) { final Intent i = new Intent(BROADCAST_REMOTE_NOTIFIER); i.putExtra("title", String.format("%s (%s)", bank.getName(), bank.getDisplayName())); i.putExtra("description", text); context.sendBroadcast(i); } // Broadcast to OpenWatch if enabled // http://forum.xda-developers.com/showthread.php?t=554551 if (prefs.getBoolean("notify_openwatch", false)) { Intent i; if (prefs.getBoolean("notify_openwatch_vibrate", false)) { i = new Intent(BROADCAST_OPENWATCH_VIBRATE); } else { i = new Intent(BROADCAST_OPENWATCH_TEXT); } i.putExtra("line1", String.format("%s (%s)", bank.getName(), bank.getDisplayName())); i.putExtra("line2", text); context.sendBroadcast(i); } // Broadcast to LiveView if enabled // http://www.sonyericsson.com/cws/products/accessories/overview/liveviewmicrodisplay if (prefs.getBoolean("notify_liveview", false)) { final Intent i = new Intent(context, LiveViewService.class); i.putExtra(LiveViewService.INTENT_EXTRA_ANNOUNCE, true); i.putExtra(LiveViewService.INTENT_EXTRA_TITLE, String.format("%s (%s)", bank.getName(), bank.getDisplayName())); i.putExtra(LiveViewService.INTENT_EXTRA_TEXT, text); context.startService(i); } } public static void broadcastTransactionUpdate(final Context context, final long bankId, final String accountId) { final Intent i = new Intent(BROADCAST_TRANSACTIONS_UPDATED); i.putExtra("accountId", Long.toString(bankId) + "_" + accountId); context.sendBroadcast(i); } public static void sendWidgetRefresh(final Context context) { // Send intent to BankdroidWidgetProvider final Intent updateIntent = new Intent(BROADCAST_WIDGET_REFRESH); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); try { pendingIntent.send(); } catch (final CanceledException e) { Timber.w(e, "Problem occurred while updating widget"); } } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); handleStart(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { handleStart(); return START_NOT_STICKY; } private void handleStart() { ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); NetworkInfo ni = cm.getActiveNetworkInfo(); if (ni != null && ni.isConnected() && shouldUpdateOnRoaming(ni)) { if (InsideUpdatePeriod()) { new DataRetrieverTask(this).execute(); } else { Timber.v("Skipping update due to not in update period."); stopSelf(); } } } private boolean shouldUpdateOnRoaming(NetworkInfo ni) { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(this); if (prefs.getBoolean("disable_during_roaming", false) && ni.isRoaming()) { return false; } return true; } private boolean InsideUpdatePeriod() { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(this); int start = prefs.getInt("refresh_start_minutes", 0); int stop = prefs.getInt("refresh_stop_minutes", 0); // If start is bigger than stop we always update. It should perhaps // be possible to set start to 17:00 and stop to 07:00 and have to // updates working from 17 to 07 around midnight if (start >= stop) { return true; } Date now = new Date(); int minutesSinceMidnight = now.getHours() * 60 + now.getMinutes(); return minutesSinceMidnight > start && minutesSinceMidnight < stop; } @Override public void onDestroy() { } @Override public IBinder onBind(final Intent intent) { return null; } static class DataRetrieverTask extends AsyncTask { private final SharedPreferences prefs; private ArrayList errors; protected final AutoRefreshService autoRefreshService; private Resources res; // This constructor is for unit testing only protected DataRetrieverTask(AutoRefreshService autoRefreshService, SharedPreferences prefs) { this.autoRefreshService = autoRefreshService; this.prefs = prefs; } public DataRetrieverTask(AutoRefreshService autoRefreshService) { this(autoRefreshService, PreferenceManager.getDefaultSharedPreferences(autoRefreshService)); } @Override protected void onPreExecute() { } protected List getBanks() { return BankFactory.banksFromDb(autoRefreshService, true); } @NonNull protected DBAdapter getDBAdapter() { return new DBAdapter(autoRefreshService); } protected void sendWidgetRefresh() { final Intent updateIntent = new Intent(BROADCAST_MAIN_REFRESH); autoRefreshService.sendBroadcast(updateIntent); AutoRefreshService.sendWidgetRefresh(autoRefreshService); } @Override protected Void doInBackground(final String... args) { errors = new ArrayList<>(); Boolean refreshWidgets = false; final List banks = getBanks(); if (banks.isEmpty()) { return null; } final DBAdapter db = getDBAdapter(); BigDecimal currentBalance; BigDecimal diff; BigDecimal minDelta = new BigDecimal(prefs.getString("notify_min_delta", "0")); final HashMap accounts = new HashMap<>(); for (final Bank bank : banks) { if (prefs.getBoolean("debug_mode", false) && prefs.getBoolean("debug_only_testbank", false)) { Timber.d( "Only_Testbank is ON. Skipping update for %s", bank.getName()); continue; } if (bank.isDisabled()) { LoggingUtils.logDisabledBank(bank); continue; } try { currentBalance = bank.getBalance(); accounts.clear(); for (final Account account : bank.getAccounts()) { accounts.put(account.getId(), account); } bank.update(); diff = currentBalance.subtract(bank.getBalance()); if (diff.compareTo(BigDecimal.ZERO) != 0) { refreshWidgets = true; } if (diff.compareTo(new BigDecimal(0)) != 0 && diff.abs().compareTo(minDelta) != -1) { Account oldAccount; for (final Account account : bank.getAccounts()) { oldAccount = accounts.get(account.getId()); if (oldAccount != null) { if (account.getBalance().compareTo( oldAccount.getBalance()) != 0) { boolean notify = false; switch (account.getType()) { case Account.REGULAR: notify = prefs.getBoolean( "notify_for_deposit", true); break; case Account.FUNDS: notify = prefs.getBoolean( "notify_for_funds", false); break; case Account.LOANS: notify = prefs.getBoolean( "notify_for_loans", false); break; case Account.CCARD: notify = prefs.getBoolean( "notify_for_ccards", true); break; case Account.OTHER: notify = prefs.getBoolean( "notify_for_other", false); break; } if (account.isHidden() || !account.isNotify()) { notify = false; } if (notify) { diff = account.getBalance().subtract( oldAccount.getBalance()); showNotification(bank, account, diff, autoRefreshService); } } } } if (prefs.getBoolean( "autoupdates_transactions_enabled", true)) { bank.updateAllTransactions(); LoggingUtils.logBankUpdate(bank, true); } else { LoggingUtils.logBankUpdate(bank, false); } } bank.closeConnection(); db.updateBank(bank); // Send update for all accounts since we're overwriting the // database transaction history if (prefs.getBoolean("content_provider_enabled", false)) { for (final Account account : bank.getAccounts()) { broadcastTransactionUpdate(autoRefreshService.getBaseContext(), bank.getDbId(), account.getId()); } } } catch (final BankException e) { // Refresh widgets if an update fails Timber.e(e, "Could not update bank %s", bank.getName()); } catch (final LoginException e) { Timber.d(e, "Invalid credentials for bank %s", bank.getName()); refreshWidgets = true; db.disableBank(bank.getDbId()); } catch (BankChoiceException e) { Timber.w(e, "BankChoiceException"); } catch (Exception e) { Timber.e(e, "An unexpected error occurred while updating bank %s", bank.getName()); } } if (refreshWidgets) { sendWidgetRefresh(); } return null; } @Override protected void onProgressUpdate(final String... args) { } @Override protected void onPostExecute(final Void unused) { if ((this.errors != null) && !this.errors.isEmpty()) { final StringBuilder errormsg = new StringBuilder(); errormsg.append(res.getText(R.string.accounts_were_not_updated) + ":\n"); for (final String err : errors) { errormsg.append(err); errormsg.append("\n"); } } Editor edit = prefs.edit(); edit.putLong("autoupdates_last_update", System.currentTimeMillis()); edit.apply(); autoRefreshService.stopSelf(); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.appwidget; import com.liato.bankdroid.Helpers; import com.liato.bankdroid.MainActivity; import com.liato.bankdroid.R; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.utils.NetworkUtils; import android.app.PendingIntent; import android.app.Service; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.view.View; import android.widget.RemoteViews; import java.io.IOException; import timber.log.Timber; public abstract class BankdroidWidgetProvider extends AppWidgetProvider { private final static String TAG = "BankdroidWidgetProvider"; private final static String ACTION_WIDGET_BLUR = "com.liato.bankdroid.action.WIDGET_BLUR"; private final static String ACTION_WIDGET_UNBLUR = "com.liato.bankdroid.action.WIDGET_UNBLUR"; static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Account account) { RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId, account); if (views != null) { appWidgetManager.updateAppWidget(appWidgetId, views); } } static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId); if (views != null) { appWidgetManager.updateAppWidget(appWidgetId, views); } } static void unblurAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); Editor e = prefs.edit(); e.putBoolean("widget_unblurred_" + appWidgetId, true); e.apply(); RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId); if (views != null) { views.setViewVisibility(R.id.imgBalanceblur, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountnameBlur, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountbalance, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountname, View.VISIBLE); appWidgetManager.updateAppWidget(appWidgetId, views); } } static void blurAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); Editor e = prefs.edit(); e.remove("widget_unblurred_" + appWidgetId); e.apply(); RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId); if (views != null) { views.setViewVisibility(R.id.imgBalanceblur, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountnameBlur, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountbalance, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountname, View.GONE); appWidgetManager.updateAppWidget(appWidgetId, views); } } static RemoteViews buildAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { String accountId = WidgetConfigureActivity.getAccountId(context, appWidgetId); long bankId = WidgetConfigureActivity.getBankId(context, appWidgetId); if (accountId == null) { Timber.w("Widget not found. ID: %s", appWidgetId); return disableAppWidget(context, appWidgetManager, appWidgetId); } Account account = BankFactory.accountFromDb(context, bankId + "_" + accountId, false); if (account == null) { Timber.w("Account not found in database"); return disableAppWidget(context, appWidgetManager, appWidgetId); } Bank bank = BankFactory.bankFromDb(account.getBankDbId(), context, false); if (bank == null) { Timber.w("Bank not found in database"); return disableAppWidget(context, appWidgetManager, appWidgetId); } account.setBank(bank); return buildAppWidget(context, appWidgetManager, appWidgetId, account); } static RemoteViews buildAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Account account) { AppWidgetProviderInfo providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId); int layoutId = (providerInfo == null) ? R.layout.widget : providerInfo.initialLayout; SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); SharedPreferences defprefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean("transperant_background" + appWidgetId, false) && (providerInfo != null)) { if (providerInfo.initialLayout == R.layout.widget_large) { layoutId = R.layout.widget_large_transparent; } else { layoutId = R.layout.widget_transparent; } } Bank bank = account.getBank(); RemoteViews views = new RemoteViews(context.getPackageName(), layoutId); views.setTextViewText(R.id.txtWidgetAccountname, account.getName().toUpperCase()); views.setTextViewText(R.id.txtWidgetAccountnameBlur, account.getName().toUpperCase()); views.setTextViewText(R.id.txtWidgetAccountbalance, Helpers.formatBalance(account.getBalance(), account.getCurrency(), defprefs.getBoolean("round_widget_balance", false), bank.getDecimalFormatter(), bank.isDisabled())); views.setImageViewResource(R.id.imgWidgetIcon, bank.getImageResource()); Intent intent = new Intent(context, MainActivity.class); PendingIntent pendingIntent; //intent = new Intent(context, AccountsActivity.class); pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.layWidgetContainer, pendingIntent); intent = new Intent(context, WidgetService.class); intent.setAction(AutoRefreshService.BROADCAST_WIDGET_REFRESH); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); intent.setData( Uri.parse("rofl://copter/" + appWidgetId + "/" + System.currentTimeMillis())); pendingIntent = PendingIntent .getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.imgWidgetIcon, pendingIntent); views.setOnClickPendingIntent(R.id.hitBox, pendingIntent); intent = new Intent(context, WidgetService.class); intent.setAction(BankdroidWidgetProvider.ACTION_WIDGET_UNBLUR); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); intent.setData(Uri.parse( "rofl://copter/widgetunblur/" + appWidgetId + "/" + System.currentTimeMillis())); pendingIntent = PendingIntent .getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.imgBalanceblur, pendingIntent); if (defprefs.getBoolean("widget_blur_balance", false) && !prefs .getBoolean("widget_unblurred_" + appWidgetId, false)) { views.setViewVisibility(R.id.imgBalanceblur, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountnameBlur, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountbalance, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountname, View.GONE); views.setOnClickPendingIntent(R.id.layWidgetContainer, pendingIntent); } else { views.setViewVisibility(R.id.imgBalanceblur, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountnameBlur, View.GONE); views.setViewVisibility(R.id.txtWidgetAccountbalance, View.VISIBLE); views.setViewVisibility(R.id.txtWidgetAccountname, View.VISIBLE); intent = new Intent(context, MainActivity.class); intent.setAction(AutoRefreshService.ACTION_MAIN_SHOW_TRANSACTIONS); intent.setData(Uri.parse("rofl://copter/showtransactions/" + appWidgetId + "/" + System .currentTimeMillis())); intent.putExtra("bank", bank.getDbId()); intent.putExtra("account", account.getId()); pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.txtWidgetAccountbalance, pendingIntent); views.setOnClickPendingIntent(R.id.layWidgetContainer, pendingIntent); } return views; } static RemoteViews disableAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { AppWidgetProviderInfo providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId); int layoutId = (providerInfo == null) ? R.layout.widget : providerInfo.initialLayout; SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); if (prefs.getBoolean("transperant_background" + appWidgetId, false) && (providerInfo != null)) { if (providerInfo.initialLayout == R.layout.widget_large) { layoutId = R.layout.widget_large_transparent; } else { layoutId = R.layout.widget_transparent; } } RemoteViews views = new RemoteViews(context.getPackageName(), layoutId); views.setTextViewText(R.id.txtWidgetAccountname, ""); views.setTextViewText(R.id.txtWidgetAccountbalance, "ERROR"); views.setImageViewResource(R.id.imgWidgetIcon, R.drawable.icon_large); Intent intent = new Intent(context, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.txtWidgetAccountbalance, pendingIntent); views.setOnClickPendingIntent(R.id.layWidgetContainer, pendingIntent); return views; } public void onReceive(Context context, Intent intent) { // v1.5 fix that doesn't call onDelete Action final String action = intent.getAction(); if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { final int appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { this.onDeleted(context, new int[]{appWidgetId}); } } else { super.onReceive(context, intent); } if (action.equals(AutoRefreshService.BROADCAST_WIDGET_REFRESH) || action .equals(android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE)) { AppWidgetManager appWM = AppWidgetManager.getInstance(context); int[] appWidgetIds = appWM.getAppWidgetIds(intent.getComponent()); final int N = appWidgetIds.length; for (int i = 0; i < N; i++) { int appWidgetId = appWidgetIds[i]; updateAppWidget(context, appWM, appWidgetId); } } } @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); final int N = appWidgetIds.length; for (int i = 0; i < N; i++) { WidgetConfigureActivity.delAccountId(context, appWidgetIds[i]); } } public static class WidgetService extends Service { @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); handleStart(intent, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { handleStart(intent, startId); return START_NOT_STICKY; } public void handleStart(Intent intent, int startId) { if (intent == null) { return; } int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); Context context = getApplicationContext(); String action = intent.getAction(); if (action == null) { return; } if (action.equals(AutoRefreshService.BROADCAST_WIDGET_REFRESH)) { new WidgetUpdateTask(context, AppWidgetManager.getInstance(context), appWidgetId) .execute(); } else if (action.equals(BankdroidWidgetProvider.ACTION_WIDGET_UNBLUR)) { unblurAppWidget(context, AppWidgetManager.getInstance(context), appWidgetId); Handler blurHandler = new Handler(); class BlurRunnable implements Runnable { private int mAppWidgetId; public BlurRunnable(int appWidgetId) { this.mAppWidgetId = appWidgetId; } @Override public void run() { Context context = getApplicationContext(); blurAppWidget(context, AppWidgetManager.getInstance(context), mAppWidgetId); } } SharedPreferences defprefs = PreferenceManager.getDefaultSharedPreferences(context); Integer unblurTimeout = 1000 * Integer .parseInt(defprefs.getString("widget_blur_balance_timeout", "5")); blurHandler.postDelayed(new BlurRunnable(appWidgetId), unblurTimeout); } else if (action.equals(BankdroidWidgetProvider.ACTION_WIDGET_BLUR)) { blurAppWidget(context, AppWidgetManager.getInstance(context), appWidgetId); } } @Override public IBinder onBind(Intent arg0) { // TODO Auto-generated method stub return null; } protected class WidgetUpdateTask extends AsyncTask { private Context context; private AppWidgetManager appWidgetManager; private int appWidgetId; private SharedPreferences prefs; public WidgetUpdateTask(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { this.context = context; this.appWidgetManager = appWidgetManager; this.appWidgetId = appWidgetId; this.prefs = PreferenceManager.getDefaultSharedPreferences(context); } @Override protected void onPreExecute() { super.onPreExecute(); RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId); if (views != null) { views.setViewVisibility(R.id.frmProgress, View.VISIBLE); appWidgetManager.updateAppWidget(appWidgetId, views); } } @Override protected Void doInBackground(Void... params) { String accountId = WidgetConfigureActivity.getAccountId(context, appWidgetId); if (accountId == null) { Timber.w("Widget not found %d", appWidgetId); return null; } long bankId = WidgetConfigureActivity.getBankId(context, appWidgetId); Bank bank = BankFactory.bankFromDb(bankId, context, true); if (bank == null) { return null; } try { if (!bank.isDisabled()) { bank.update(); if (prefs.getBoolean("widget_updates_transactions", false)) { bank.updateAllTransactions(); } bank.closeConnection(); DBAdapter.save(bank, context); } } catch (BankException e) { Timber.e(e, "Could not update bank %s", bank.getName()); } catch (LoginException e) { Timber.w(e, "Invalid credentials for bank %s", bank.getName()); DBAdapter.disable(bank, context); } catch (BankChoiceException e) { Timber.w(e, "BankChoiceException"); } catch (IOException e) { if (NetworkUtils.isInternetAvailable()) { Timber.e(e, "Could not update bank %s", bank.getName()); } } BankdroidWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId); return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); RemoteViews views = buildAppWidget(context, appWidgetManager, appWidgetId); if (views != null) { views.setViewVisibility(R.id.frmProgress, View.INVISIBLE); appWidgetManager.updateAppWidget(appWidgetId, views); } WidgetService.this.stopSelf(); } } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider_2x1.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.appwidget; public class BankdroidWidgetProvider_2x1 extends BankdroidWidgetProvider { } ================================================ FILE: app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider_4x1.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.appwidget; public class BankdroidWidgetProvider_4x1 extends BankdroidWidgetProvider { } ================================================ FILE: app/src/main/java/com/liato/bankdroid/appwidget/WidgetConfigureActivity.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.appwidget; import com.liato.bankdroid.LockableActivity; import com.liato.bankdroid.R; import com.liato.bankdroid.adapters.AccountsAdapter; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.BankFactory; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CheckBox; import android.widget.ListView; import java.util.ArrayList; public class WidgetConfigureActivity extends LockableActivity { private static final String WIDGET_PREFIX = "widget_"; int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; private AccountsAdapter adapter; public WidgetConfigureActivity() { super(); } public static void setAccountBankId(Context context, int appWidgetId, String accountId, long bankId) { SharedPreferences.Editor prefs = context.getSharedPreferences("widget_prefs", 0).edit(); prefs.putString(WIDGET_PREFIX + appWidgetId, accountId); prefs.putLong(WIDGET_PREFIX + appWidgetId + "_bankid", bankId); prefs.apply(); } public static String getAccountId(Context context, int appWidgetId) { SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); return prefs.getString(WIDGET_PREFIX + appWidgetId, null); } public static long getBankId(Context context, int appWidgetId) { SharedPreferences prefs = context.getSharedPreferences("widget_prefs", 0); return prefs.getLong(WIDGET_PREFIX + appWidgetId + "_bankid", -1); } public static void delAccountId(Context context, int appWidgetId) { SharedPreferences.Editor prefs = context.getSharedPreferences("widget_prefs", 0).edit(); prefs.remove(WIDGET_PREFIX + appWidgetId); prefs.remove(WIDGET_PREFIX + appWidgetId + "_bankid"); prefs.apply(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } public void onResume() { super.onResume(); setContentView(R.layout.main); this.setTitle(this.getString(R.string.choose_an_account)); setResult(RESULT_CANCELED); ((View) findViewById(R.id.layMainMenu)).setVisibility(View.GONE); Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish(); } ListView lv = (ListView) findViewById(R.id.lstAccountsList); lv.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View view, int position, long id) { if (adapter.getItemViewType(position) != AccountsAdapter.VIEWTYPE_ACCOUNT) { return; } final Context context = WidgetConfigureActivity.this; Account account = (Account) parent.getItemAtPosition(position); Bank bank = account.getBank(); WidgetConfigureActivity .setAccountBankId(context, mAppWidgetId, account.getId(), bank.getDbId()); SharedPreferences.Editor prefs = context.getSharedPreferences("widget_prefs", 0) .edit(); prefs.putBoolean("transperant_background" + mAppWidgetId, ((CheckBox) findViewById(R.id.chkTransperantBackground)).isChecked()); prefs.apply(); // Push widget update to surface with newly set prefix AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); BankdroidWidgetProvider.updateAppWidget(context, appWidgetManager, mAppWidgetId, account); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); finish(); } }); refreshView(); } private void refreshView() { ArrayList banks = BankFactory.banksFromDb(this, true); if (banks.size() > 0) { findViewById(R.id.chkTransperantBackground).setVisibility(View.VISIBLE); findViewById(R.id.txtAccountsDesc).setVisibility(View.GONE); ListView lv = (ListView) findViewById(R.id.lstAccountsList); adapter = new AccountsAdapter(this, false); adapter.setGroups(banks); lv.setAdapter(adapter); } } @Override public boolean shouldShowActionBar() { return false; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/banking/BankFactory.java ================================================ /* * Copyright (C) 2014 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.banking; import com.crashlytics.android.answers.CustomEvent; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.db.Crypto; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.db.Database; import com.liato.bankdroid.db.DatabaseHelper; import com.liato.bankdroid.utils.LoggingUtils; import net.sf.andhsli.hotspotlogin.SimpleCrypto; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.Nullable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import timber.log.Timber; public class BankFactory { private static Bank fromBanktypeId(int id, Context context) throws BankException { return LegacyBankFactory.fromBanktypeId(id, context); } public static List listBanks(Context context) { return LegacyBankFactory.listBanks(context); } @Nullable public static Bank bankFromDb(long id, Context context, boolean loadAccounts) { Bank bank = null; DBAdapter db = new DBAdapter(context); Cursor c = db.getBank(id); if (c != null && c.getCount() > 0) { try { bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context); bank.setProperties(loadProperties(id, context)); bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))), (c.getInt(c.getColumnIndex("disabled")) != 0), c.getLong(c.getColumnIndex("_id")), c.getString(c.getColumnIndex("currency")), c.getString(c.getColumnIndex("custname")), c.getInt(c.getColumnIndex("hideAccounts"))); if (loadAccounts) { bank.setAccounts(accountsFromDb(context, bank.getDbId())); } } catch (BankException e) { Timber.w(e, "Failed getting bank from database"); } finally { c.close(); } } return bank; } public static ArrayList banksFromDb(Context context, boolean loadAccounts) { ArrayList banks = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchBanks(); try { if (c == null || c.getCount() == 0) { return banks; } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); try { Bank bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context); long id = c.getLong(c.getColumnIndex("_id")); bank.setProperties(loadProperties(id, context)); bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))), (c.getInt(c.getColumnIndex("disabled")) != 0), id, c.getString(c.getColumnIndex("currency")), c.getString(c.getColumnIndex("custname")), c.getInt(c.getColumnIndex("hideAccounts"))); if (loadAccounts) { bank.setAccounts(accountsFromDb(context, bank.getDbId())); } banks.add(bank); } catch (BankException e) { Timber.w(e, "BankFactory.banksFromDb()"); } } } finally { if (c != null) { c.close(); } } return banks; } @Nullable public static Account accountFromDb(Context context, String accountId, boolean loadTransactions) { DBAdapter db = new DBAdapter(context); Cursor ac = db.getAccount(accountId); Account account; try { if (ac == null || ac.isClosed() || (ac.isBeforeFirst() && ac.isAfterLast())) { return null; } account = new Account(ac.getString(ac.getColumnIndex("name")), new BigDecimal(ac.getString(ac.getColumnIndex("balance"))), ac.getString(ac.getColumnIndex("id")).split("_", 2)[1], ac.getLong(ac.getColumnIndex("bankid")), ac.getInt(ac.getColumnIndex("acctype"))); account.setHidden(ac.getInt(ac.getColumnIndex("hidden")) == 1); account.setNotify(ac.getInt(ac.getColumnIndex("notify")) == 1); account.setCurrency(ac.getString(ac.getColumnIndex("currency"))); account.setAliasfor(ac.getString(ac.getColumnIndex("aliasfor"))); } finally { if (ac != null) { ac.close(); } } if (loadTransactions) { ArrayList transactions = new ArrayList<>(); String fromAccount = accountId; if (account.getAliasfor() != null && account.getAliasfor().length() > 0) { fromAccount = Long.toString(account.getBankDbId()) + "_" + account.getAliasfor(); } Cursor tc = db.fetchTransactions(fromAccount); try { if (!(tc == null || tc.isClosed() || (tc.isBeforeFirst() && tc.isAfterLast()))) { while (!tc.isLast() && !tc.isAfterLast()) { tc.moveToNext(); transactions.add(new Transaction(tc.getString(tc.getColumnIndex("transdate")), tc.getString(tc.getColumnIndex("btransaction")), new BigDecimal(tc.getString(tc.getColumnIndex("amount"))), tc.getString(tc.getColumnIndex("currency")))); } } } finally { if (tc != null) { tc.close(); } } account.setTransactions(transactions); } return account; } private static ArrayList accountsFromDb(Context context, long bankId) { ArrayList accounts = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchAccounts(bankId); try { if (c == null || c.getCount() == 0) { return accounts; } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); try { Account account = new Account(c.getString(c.getColumnIndex("name")), new BigDecimal(c.getString(c.getColumnIndex("balance"))), c.getString(c.getColumnIndex("id")).split("_", 2)[1], c.getLong(c.getColumnIndex("bankid")), c.getInt(c.getColumnIndex("acctype"))); account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1); account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1); account.setCurrency(c.getString(c.getColumnIndex("currency"))); account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); accounts.add(account); } catch (ArrayIndexOutOfBoundsException e) { // Probably an old Avanza account Timber.w(e, "Attempted to load an account without an ID: %d", bankId); } } } finally { if (c != null) { c.close(); } } return accounts; } private static Map loadProperties(long bankId, Context context) { Map properties = new HashMap<>(); Map decryptedProperties = new HashMap<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchProperties(Long.toString(bankId)); try { if (c == null || c.getCount() == 0) { return properties; } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); String key = c.getString(c.getColumnIndex(Database.PROPERTY_KEY)); String value = c.getString(c.getColumnIndex(Database.PROPERTY_VALUE)); if (LegacyProviderConfiguration.PASSWORD.equals(key)) { try { value = SimpleCrypto.decrypt(Crypto.getKey(), value); decryptedProperties.put(key, value); } catch (Exception e) { Timber.i("%s %s", "Failed decrypting bank properties.", "This usually means they are unencrypted, which is exactly what we want them to be."); } } properties.put(key, value); } } finally { if (c != null) { c.close(); } } storeDecryptedProperties(context, bankId, decryptedProperties); return properties; } /** * Stores decrypted passwords on disk. *

* This is a step in removing password encryption alltogether. *

* The background is that it's broken on Androin Nougat anyway, and that it * didn't provide any extra security before that either. *

* Since Bankdroid needs to send plain text passwords to the banks, it must * be possible to retrieve the plain text passwords automatically. And if the * passwords are encrypted on disk, Bankdroid needs to have the key. And if * Bankdroid stores both the key and the encrypted password on the phone, a * determined attacker could get both anyway, and the encryption is useless. *

* The only thing the encryption has protected against is a using rooting * their own device and retrieving their own plain text passwords. This would * enable the attacker to reaa their own account balance from the bank. Which * they likely already could even before this change... */ private static void storeDecryptedProperties( Context context, long bankId, Map decryptedProperties) { if (decryptedProperties.isEmpty()) { return; } Timber.i("Storing %d decrypted properties...", decryptedProperties.size()); SQLiteDatabase db = DatabaseHelper.getHelper(context).getWritableDatabase(); for (Map.Entry property : decryptedProperties.entrySet()) { String value = property.getValue(); if (value != null && !value.isEmpty()) { ContentValues propertyValues = new ContentValues(); propertyValues.put(Database.PROPERTY_KEY, property.getKey()); propertyValues.put(Database.PROPERTY_VALUE, value); propertyValues.put(Database.PROPERTY_CONNECTION_ID, bankId); db.insertWithOnConflict( Database.PROPERTY_TABLE_NAME, null, propertyValues, SQLiteDatabase.CONFLICT_REPLACE); } } Timber.i("%d decrypted properties stored", decryptedProperties.size()); LoggingUtils.logCustom(new CustomEvent("Passwords Decrypted")); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/db/DBAdapter.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.db; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.Transaction; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.Nullable; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Map; public class DBAdapter { private SQLiteDatabase mDb; /** * Constructor - takes the context to allow the database to be * opened/created * * @param ctx the Context within which to work */ public DBAdapter(Context ctx) { DatabaseHelper dbHelper = DatabaseHelper.getHelper(ctx); mDb = dbHelper.getWritableDatabase(); } /** * @deprecated Only used during refactoring. Should be removed before next major version (2.0) */ @Deprecated public static void save(Bank bank, Context context) { DBAdapter db = new DBAdapter(context); long id = db.updateBank(bank); bank.setDbid(id); } /** * @deprecated Only used during refactoring. Should be removed before next major version (2.0) */ @Deprecated public static void disable(Bank bank, Context context) { DBAdapter db = new DBAdapter(context); db.disableBank(bank.getDbId()); } public long createBank(Bank bank) { return updateBank(bank); } /** * Delete the bank with the given bankId * * @param bankId id of bank to delete */ public int deleteBank(long bankId) { int c = mDb.delete("banks", "_id=" + bankId, null); c += this.deleteAccounts(bankId); return c; } /** * Delete the accounts for the given bankIdbank with the given rowId * * @param bankId id of bank to delete */ public int deleteAccounts(long bankId) { int c = mDb.delete("accounts", "bankid=" + bankId, null); return c; } public int deleteTransactions(String account) { int c = mDb.delete("transactions", "account='" + account + "'", null); return c; } private int deleteProperties(long bankId) { return mDb.delete(Database.PROPERTY_TABLE_NAME, Database.PROPERTY_CONNECTION_ID + "=" + bankId, null); } /** * Return a Cursor over the list of all banks in the database * * @return Cursor over all banks */ public Cursor fetchBanks() { return mDb.query("banks", new String[]{"_id", "balance", "banktype", "disabled", "custname", "updated", "sortorder", "currency", "hideAccounts"}, null, null, null, null, "_id asc"); } /** * Return a Cursor over the list of all accounts belonging to a bank * * @return Cursor over all accounts belonging to a bank */ public Cursor fetchAccounts(long bankId) { return mDb.query("accounts", new String[]{"bankid", "balance", "name", "id", "acctype", "hidden", "notify", "currency", "aliasfor"}, "bankid=" + bankId, null, null, null, null); } public Cursor fetchTransactions(String account) { return mDb.query("transactions", new String[]{"transdate", "btransaction", "amount", "currency"}, "account='" + account + "'", null, null, null, null); } public Cursor fetchProperties(String bankId) { return mDb.query(Database.PROPERTY_TABLE_NAME, null, Database.PROPERTY_CONNECTION_ID + "='" + bankId + "'", null, null, null, null); } public long updateBank(Bank bank) { Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ContentValues initialValues = new ContentValues(); initialValues.put("banktype", bank.getBanktypeId()); initialValues.put("disabled", 0); initialValues.put("balance", bank.getBalance().toPlainString()); initialValues.put("currency", bank.getCurrency()); initialValues.put("custname", bank.getCustomName()); initialValues.put("updated", sdf.format(cal.getTime())); initialValues.put("hideAccounts", bank.getHideAccounts() ? 1 : 0); long bankId = bank.getDbId(); if (bankId == -1) { bankId = mDb.insert("banks", null, initialValues); } else { mDb.update("banks", initialValues, "_id=" + bankId, null); deleteAccounts(bankId); deleteProperties(bankId); } if (bankId != -1) { Map properties = bank.getProperties(); for (Map.Entry property : properties.entrySet()) { String value = property.getValue(); if (value != null && !value.isEmpty()) { ContentValues propertyValues = new ContentValues(); propertyValues.put(Database.PROPERTY_KEY, property.getKey()); propertyValues.put(Database.PROPERTY_VALUE, value); propertyValues.put(Database.PROPERTY_CONNECTION_ID, bankId); mDb.insert(Database.PROPERTY_TABLE_NAME, null, propertyValues); } } ArrayList accounts = bank.getAccounts(); for (Account acc : accounts) { ContentValues vals = new ContentValues(); vals.put("bankid", bankId); vals.put("balance", acc.getBalance().toPlainString()); vals.put("name", acc.getName()); vals.put("id", bankId + "_" + acc.getId()); vals.put("hidden", acc.isHidden() ? 1 : 0); vals.put("notify", acc.isNotify() ? 1 : 0); vals.put("currency", acc.getCurrency()); vals.put("acctype", acc.getType()); vals.put("aliasfor", acc.getAliasfor()); mDb.insert("accounts", null, vals); if (acc.getAliasfor() == null || acc.getAliasfor().length() == 0) { List transactions = acc.getTransactions(); if (transactions != null && !transactions.isEmpty()) { deleteTransactions(bankId + "_" + acc.getId()); for (Transaction transaction : transactions) { ContentValues transvals = new ContentValues(); transvals.put("transdate", transaction.getDate()); transvals.put("btransaction", transaction.getTransaction()); transvals.put("amount", transaction.getAmount().toPlainString()); transvals.put("account", bankId + "_" + acc.getId()); transvals.put("currency", transaction.getCurrency()); mDb.insert("transactions", null, transvals); } } } } } return bankId; } public void disableBank(long bankId) { if (bankId == -1) { return; } ContentValues initialValues = new ContentValues(); initialValues.put("disabled", 1); mDb.update("banks", initialValues, "_id=" + bankId, null); } @Nullable public Cursor getBank(String bankId) { Cursor c = mDb.query("banks", new String[]{"_id", "balance", "banktype", "disabled", "custname", "updated", "sortorder", "currency", "hideAccounts"}, "_id=" + bankId, null, null, null, null); if (c != null) { c.moveToFirst(); } return c; } @Nullable public Cursor getBank(long bankId) { return getBank(Long.toString(bankId)); } @Nullable public Cursor getAccount(String id) { Cursor c = mDb.query("accounts", new String[]{"id", "balance", "name", "bankid", "acctype", "hidden", "notify", "currency", "aliasfor"}, "id='" + id + "'", null, null, null, null); if (c != null) { c.moveToFirst(); } return c; } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/db/Database.java ================================================ package com.liato.bankdroid.db; public class Database { static final String DATABASE_NAME = "data"; static final int DATABASE_VERSION = 12; public static final String PROPERTY_TABLE_NAME = "connection_properties"; public static final String PROPERTY_CONNECTION_ID = "connection_id"; public static final String PROPERTY_KEY = "property"; public static final String PROPERTY_VALUE = "value"; static final String TABLE_CONNECTION_PROPERTIES = "CREATE TABLE " + PROPERTY_TABLE_NAME + " (" + PROPERTY_CONNECTION_ID + " INTEGER REFERENCES " + LegacyDatabase.BANK_TABLE_NAME + "(" + LegacyDatabase.BANK_ID + ") ON DELETE CASCADE, " + PROPERTY_KEY + " TEXT NOT NULL, " + PROPERTY_VALUE + " TEXT, " + "PRIMARY KEY (" + PROPERTY_CONNECTION_ID + "," + PROPERTY_KEY + "));"; } ================================================ FILE: app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java ================================================ /* * Copyright (C) 2010 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.db; import com.liato.bankdroid.banking.LegacyProviderConfiguration; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import timber.log.Timber; import static com.liato.bankdroid.db.Database.PROPERTY_CONNECTION_ID; import static com.liato.bankdroid.db.Database.PROPERTY_KEY; import static com.liato.bankdroid.db.Database.PROPERTY_TABLE_NAME; import static com.liato.bankdroid.db.Database.PROPERTY_VALUE; /** * @since 8 jan 2011 */ final public class DatabaseHelper extends SQLiteOpenHelper { private static DatabaseHelper instance; private DatabaseHelper(final Context context) { super(context, Database.DATABASE_NAME, null, Database.DATABASE_VERSION); } public static synchronized DatabaseHelper getHelper(Context context) { if (instance == null) { instance = new DatabaseHelper(context); } return instance; } @Override public void onCreate(final SQLiteDatabase db) { db.execSQL(LegacyDatabase.TABLE_BANKS); db.execSQL(LegacyDatabase.TABLE_ACCOUNTS); db.execSQL(LegacyDatabase.TABLE_TRANSACTIONS); db.execSQL(Database.TABLE_CONNECTION_PROPERTIES); } @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { Timber.d("Upgrading database from version %d to %d", newVersion, oldVersion); // Version <= 1.7.2 if (oldVersion <= 9) { // Add an "extras" field to the bank and and "alias for" field to the account. db.execSQL("ALTER TABLE " + LegacyDatabase.BANK_TABLE_NAME + " ADD " + LegacyDatabase.BANK_EXTRAS + " text;"); db.execSQL("ALTER TABLE " + LegacyDatabase.ACCOUNT_TABLE_NAME + " ADD " + LegacyDatabase.ACCOUNT_ALIAS_FOR + " text;"); } if (oldVersion <= 10) { db.execSQL("ALTER TABLE " + LegacyDatabase.BANK_TABLE_NAME + " ADD " + LegacyDatabase.BANK_HIDE_ACCOUNTS + " integer;"); } if (oldVersion <= 11) { try { db.beginTransaction(); db.execSQL(Database.TABLE_CONNECTION_PROPERTIES); migrateProperties(db); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } private void migrateProperties(final SQLiteDatabase db) { String tempTable = LegacyDatabase.BANK_TABLE_NAME + "_temp"; db.execSQL("ALTER TABLE " + LegacyDatabase.BANK_TABLE_NAME + " RENAME TO " + tempTable + ";"); // Drop username, password and extras fields from bank table db.execSQL(LegacyDatabase.TABLE_BANKS); db.execSQL("INSERT INTO " + LegacyDatabase.BANK_TABLE_NAME + " SELECT " + LegacyDatabase.BANK_ID + "," + LegacyDatabase.BANK_BALANCE + "," + LegacyDatabase.BANK_TYPE + "," + LegacyDatabase.BANK_CUSTOM_NAME + "," + LegacyDatabase.BANK_UPDATED + "," + LegacyDatabase.BANK_SORT_ORDER + "," + LegacyDatabase.BANK_CURRENCY + "," + LegacyDatabase.BANK_DISABLED + "," + LegacyDatabase.BANK_HIDE_ACCOUNTS + " FROM " + tempTable); // Add username, password and extras fields to properties table. Cursor c = db.query(tempTable, null, null, null, null, null, null); try { if (!(c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast()))) { while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); long id = c.getLong(c.getColumnIndex(LegacyDatabase.BANK_ID)); ContentValues usernameProperty = new ContentValues(); usernameProperty.put(PROPERTY_CONNECTION_ID, id); usernameProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.USERNAME); usernameProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_USERNAME))); db.insert(PROPERTY_TABLE_NAME, null, usernameProperty); ContentValues passwordProperty = new ContentValues(); passwordProperty.put(PROPERTY_CONNECTION_ID, id); passwordProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.PASSWORD); passwordProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_PASSWORD))); db.insert(PROPERTY_TABLE_NAME, null, passwordProperty); String extras = c.getString(c.getColumnIndex(LegacyDatabase.BANK_EXTRAS)); if (extras != null && !extras.isEmpty()) { ContentValues extrasProperty = new ContentValues(); extrasProperty.put(PROPERTY_CONNECTION_ID, id); extrasProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.EXTRAS); extrasProperty.put(PROPERTY_VALUE, extras); db.insert(PROPERTY_TABLE_NAME, null, extrasProperty); } } } } finally { if (c != null) { c.close(); } } db.execSQL("DROP TABLE " + tempTable); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/db/LegacyDatabase.java ================================================ package com.liato.bankdroid.db; class LegacyDatabase { static final String BANK_TABLE_NAME = "banks"; static final String BANK_ID = "_id"; static final String BANK_BALANCE = "balance"; static final String BANK_TYPE = "banktype"; static final String BANK_USERNAME = "username"; static final String BANK_PASSWORD = "password"; static final String BANK_CUSTOM_NAME = "custname"; static final String BANK_UPDATED = "updated"; static final String BANK_SORT_ORDER = "sortorder"; static final String BANK_CURRENCY = "currency"; static final String BANK_DISABLED = "disabled"; static final String BANK_HIDE_ACCOUNTS = "hideAccounts"; static final String BANK_EXTRAS = "extras"; static final String ACCOUNT_TABLE_NAME = "accounts"; static final String ACCOUNT_BANK_ID = "bankid"; static final String ACCOUNT_ID = "id"; static final String ACCOUNT_BALANCE = "balance"; static final String ACCOUNT_CURRENCY = "currency"; static final String ACCOUNT_TYPE = "acctype"; static final String ACCOUNT_NAME = "name"; static final String ACCOUNT_HIDDEN = "hidden"; static final String ACCOUNT_NOTIFY = "notify"; static final String ACCOUNT_ALIAS_FOR = "aliasfor"; static final String TRANSACTION_TABLE_NAME = "transactions"; static final String TRANSACTION_ID = "_id"; static final String TRANSACTION_DATE = "transdate"; static final String TRANSACTION_DESCRIPTION = "btransaction"; static final String TRANSACTION_AMOUNT = "amount"; static final String TRANSACTION_CURRENCY = "currency"; static final String TRANSACTION_ACCOUNT_ID = "account"; static final String TABLE_BANKS = new StringBuilder("create table ") .append(BANK_TABLE_NAME) .append(" (") .append(BANK_ID) .append(" integer primary key autoincrement, ") .append(BANK_BALANCE) .append(" text not null, ") .append(BANK_TYPE) .append(" integer not null, ") .append(BANK_CUSTOM_NAME) .append(" text, ") .append(BANK_UPDATED) .append(" text, ") .append(BANK_SORT_ORDER) .append(" real, ") .append(BANK_CURRENCY) .append(" text, ") .append(BANK_DISABLED) .append(" integer, ") .append(BANK_HIDE_ACCOUNTS) .append(" integer);").toString(); static final String TABLE_ACCOUNTS = new StringBuilder("create table ") .append(ACCOUNT_TABLE_NAME) .append(" (") .append(ACCOUNT_BANK_ID) .append(" integer not null, ") .append(ACCOUNT_ID) .append(" text not null, ") .append(ACCOUNT_BALANCE) .append(" text not null, ") .append(ACCOUNT_TYPE) .append(" integer not null, ") .append(ACCOUNT_HIDDEN) .append(" integer not null, ") .append(ACCOUNT_NOTIFY) .append(" integer not null, ") .append(ACCOUNT_CURRENCY) .append(" text, ") .append(ACCOUNT_NAME) .append(" text not null, ") .append(ACCOUNT_ALIAS_FOR) .append(" text);").toString(); static final String TABLE_TRANSACTIONS = new StringBuilder("create table ") .append(TRANSACTION_TABLE_NAME) .append(" (") .append(TRANSACTION_ID) .append(" integer primary key autoincrement, ") .append(TRANSACTION_DATE) .append(" text not null, ") .append(TRANSACTION_DESCRIPTION) .append(" text not null, ") .append(TRANSACTION_AMOUNT) .append(" text not null, ") .append(TRANSACTION_CURRENCY) .append(" text, ") .append(TRANSACTION_ACCOUNT_ID) .append(" text not null);").toString(); private LegacyDatabase() { } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/liveview/LiveViewService.java ================================================ /* * Copyright (C) 2011 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.liato.bankdroid.liveview; import com.liato.bankdroid.MainActivity; import com.liato.bankdroid.R; import com.sonyericsson.extras.liveview.IPluginServiceCallbackV1; import com.sonyericsson.extras.liveview.IPluginServiceV1; import android.app.Service; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import timber.log.Timber; /** * Implementation of the Live View plug-in service. * * @author firetech */ public class LiveViewService extends Service { // Announce intent information keys public static final String INTENT_EXTRA_ANNOUNCE = "isAnnounce"; public static final String INTENT_EXTRA_TITLE = "title"; public static final String INTENT_EXTRA_TEXT = "text"; // Template menu icon file name. private static final String MENU_ICON_FILENAME = "plugin_icon.png"; // There should only be one instance of the service protected static boolean alreadyRunning = false; // Plugin name protected String mPluginName = null; // Current plugin Id protected int mPluginId = 0; // Menu icon that will be shown in LiveView unit protected String mMenuIcon = null; // Reference to LiveView application stub private IPluginServiceV1 mLiveView = null; /** * The service connection that is used to bind the plugin to the LiveView * service. * * When connected to the service, the plugin is registered. When * disconnected to the service, the plugin is unregistered. */ private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(final ComponentName className, IBinder service) { Timber.d("Enter LiveViewService.ServiceConnection.onServiceConnected."); mLiveView = IPluginServiceV1.Stub.asInterface(service); // Init adapter LiveViewCallback lvCallback = new LiveViewCallback(); // Install plugin try { if (mLiveView != null) { // Register mPluginId = mLiveView .register(lvCallback, mMenuIcon, mPluginName, false, getPackageName()); Timber.d("Plugin registered with id: %d", mPluginId); } } catch (RemoteException re) { Timber.e("Failed to install plugin. Stop self."); stopSelf(); } Timber.d("Plugin registered. mPluginId: %d", mPluginId); } @Override public void onServiceDisconnected(ComponentName className) { Timber.d("Enter LiveViewService.ServiceConnection.onServiceDisconnected."); stopSelf(); } }; /** * Check if service is already running. * * @return running? */ public static boolean isAlreadyRunning() { return alreadyRunning; } public void onCreate() { super.onCreate(); Timber.d("Enter LiveViewService.onCreate."); // Load menu icon int iconId = R.drawable.ic_launcher; mMenuIcon = PluginUtils.storeIconToFile(this, getResources(), iconId, MENU_ICON_FILENAME); } public void onDestroy() { Timber.d("Enter LiveViewService.onDestroy."); // Unbind from LiveView service if (mServiceConnection != null) { unbindService(mServiceConnection); } // No longer a running service alreadyRunning = false; super.onDestroy(); } public void onStart(Intent intent, int startId) { super.onStart(intent, startId); Timber.d("Enter LiveViewService.onStart."); if (intent == null) { return; } if (intent.getBooleanExtra(INTENT_EXTRA_ANNOUNCE, false)) { Bundle extras = intent.getExtras(); if (extras != null) { try { if (mLiveView != null) { mLiveView.sendAnnounce(mPluginId, mMenuIcon, extras.getString(INTENT_EXTRA_TITLE), extras.getString(INTENT_EXTRA_TEXT), System.currentTimeMillis(), ""); Timber.d("Announce sent to LiveView Application"); } else { Timber.d("LiveView Application not reachable"); } } catch (Exception e) { Timber.e(e, "Failed to send announce"); } } } else { // We end up here when LiveView Application probes the plugin if (isAlreadyRunning()) { Timber.d("Already started."); } else { // Init mPluginName = getResources().getString(R.string.app_name); // Bind to LiveView connectToLiveView(); // Singleton alreadyRunning = true; } } } @Override public IBinder onBind(final Intent intent) { Timber.d("Enter LiveViewService.onBind."); return null; } /** * Connects to the LiveView service. */ private void connectToLiveView() { boolean result = bindService(new Intent(PluginConstants.LIVEVIEW_SERVICE_BIND_INTENT), mServiceConnection, 0); if (result) { Timber.d("Bound to LiveView."); } else { Timber.d("No bind."); stopSelf(); } } /** * When a user presses the "open in phone" button on the LiveView device, this method is * called. * * Opens the MainActivity on the phone. */ protected void openInPhone(String openInPhoneAction) { Timber.d("openInPhone"); Intent i = new Intent(this, MainActivity.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i); } /** * LiveView callback interface method. */ private class LiveViewCallback extends IPluginServiceCallbackV1.Stub { Handler mCallbackHandler = new Handler(); public String getPluginName() throws RemoteException { return mPluginName; } public void openInPhone(final String openInPhoneAction) throws RemoteException { mCallbackHandler.post(new Runnable() { public void run() { LiveViewService.this.openInPhone(openInPhoneAction); } }); } //Unused methods required by API. public void startPlugin() throws RemoteException { } public void stopPlugin() throws RemoteException { } public void onUnregistered() throws RemoteException { } public void displayCaps(int displayWidthPixels, int displayHeigthPixels) throws RemoteException { } public void button(String buttonType, boolean doublepress, boolean longpress) throws RemoteException { } public void screenMode(int screenMode) throws RemoteException { } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/liveview/PluginConstants.java ================================================ /* * Copyright (C) 2011 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.liato.bankdroid.liveview; /** * Plugin common constants. * * @author firetech */ public final class PluginConstants { // Broadcast receiver constants public static final String BROADCAST_COMMAND = "CMD"; public static final String BROADCAST_COMMAND_PREFERENCES = "preferences"; public static final String BROADCAST_COMMAND_START = "start"; public static final String BROADCAST_COMMAND_PLUGIN_NAME = "pluginName"; // LiveView Plugin interface intents public static final String LIVEVIEW_SERVICE_BIND_INTENT = "com.sonyericsson.extras.liveview.PLUGIN_SERVICE_V1"; public static final String LIVEVIEW_BROADCAST_LAUNCH_EVENT = "com.sonyericsson.extras.liveview.LAUNCH_PLUGIN"; // Log tag public static final String LOG_TAG = "BankDroidLiveViewPlugin"; private PluginConstants() { } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/liveview/PluginReceiver.java ================================================ /* * Copyright (C) 2011 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.liato.bankdroid.liveview; import com.liato.bankdroid.R; import com.liato.bankdroid.SettingsActivity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import timber.log.Timber; /** * Receives broadcast intents from LiveView service. * * @author firetech */ public class PluginReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String command = intent.getExtras().getString(PluginConstants.BROADCAST_COMMAND); Timber.v("Received command: %s", command); if (command == null) { return; } if (command.contentEquals(PluginConstants.BROADCAST_COMMAND_PREFERENCES)) { String pluginName = intent.getExtras() .getString(PluginConstants.BROADCAST_COMMAND_PLUGIN_NAME); String myPluginName = context.getResources().getString(R.string.app_name); if (pluginName != null && pluginName.contentEquals(myPluginName)) { Timber.v("Starting preferences!"); Intent prefsIntent = new Intent(context, SettingsActivity.class); prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(prefsIntent); } } else if (command.contentEquals(PluginConstants.BROADCAST_COMMAND_START)) { if (LiveViewService.isAlreadyRunning()) { Timber.v("Service is already running."); } else { Timber.v("Starting service!"); context.startService(new Intent(context, LiveViewService.class)); } } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/liveview/PluginUtils.java ================================================ /* * Copyright (C) 2011 Nullbyte * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright (c) 2010 Sony Ericsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.liato.bankdroid.liveview; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import timber.log.Timber; /** * Utils for LiveView plugin. * * @author firetech */ public final class PluginUtils { private PluginUtils() { } /** * Stores icon to phone file system * * @param resources Reference to project resources * @param resource Reference to specific resource * @param fileName The icon file name */ public static String storeIconToFile(Context ctx, Resources resources, int resource, String fileName) { Timber.d("Store icon to file."); if (resources == null) { return ""; } Bitmap bitmap = BitmapFactory.decodeStream(resources.openRawResource(resource)); try { FileOutputStream fos = ctx.openFileOutput(fileName, Context.MODE_WORLD_READABLE); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); fos.flush(); fos.close(); } catch (IOException e) { Timber.e(e, "Failed to store to device"); } File iconFile = ctx.getFileStreamPath(fileName); Timber.d("Icon stored. %s", iconFile.getAbsolutePath()); return iconFile.getAbsolutePath(); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPattern.java ================================================ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.google.common.collect.Lists; import com.liato.bankdroid.R; import com.liato.bankdroid.lockpattern.LockPatternView.DisplayMode; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.widget.TextView; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * If the user has a lock pattern set already, makes them confirm the existing one. * * Then, prompts the user to choose a lock pattern: * - prompts for initial pattern * - asks for confirmation / restart * - saves chosen password when confirmed */ public class ChooseLockPattern extends Activity implements View.OnClickListener { /** * Used by the choose lock pattern wizard to indicate the wizard is * finished, and each activity in the wizard should finish. *

* Previously, each activity in the wizard would finish itself after * starting the next activity. However, this leads to broken 'Back' * behavior. So, now an activity does not finish itself until it gets this * result. */ static final int RESULT_FINISHED = RESULT_FIRST_USER; // how long after a confirmation message is shown before moving on static final int INFORMATION_MSG_TIMEOUT_MS = 3000; // how long we wait to clear a wrong pattern private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; private static final int ID_EMPTY_MESSAGE = -1; private static final String KEY_UI_STAGE = "uiStage"; private static final String KEY_PATTERN_CHOICE = "chosenPattern"; /** * The patten used during the help screen to show how to draw a pattern. */ private final List mAnimatePattern = Collections.unmodifiableList( Lists.newArrayList( LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1), LockPatternView.Cell.of(1, 1), LockPatternView.Cell.of(2, 1) )); protected TextView mHeaderText; protected LockPatternView mLockPatternView; protected TextView mFooterText; protected List mChosenPattern = null; protected LockPatternUtils mLockPatternUtils; private TextView mFooterLeftButton; private TextView mFooterRightButton; private Stage mUiStage = Stage.Introduction; private Runnable mClearPatternRunnable = new Runnable() { public void run() { mLockPatternView.clearPattern(); } }; /** * The pattern listener that responds according to a user choosing a new * lock pattern. */ protected LockPatternView.OnPatternListener mChooseNewLockPatternListener = new LockPatternView.OnPatternListener() { public void onPatternStart() { mLockPatternView.removeCallbacks(mClearPatternRunnable); patternInProgress(); } public void onPatternCleared() { mLockPatternView.removeCallbacks(mClearPatternRunnable); } public void onPatternDetected(List pattern) { if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) { if (mChosenPattern == null) { throw new IllegalStateException( "null chosen pattern in stage 'need to confirm"); } if (mChosenPattern.equals(pattern)) { updateStage(Stage.ChoiceConfirmed); } else { updateStage(Stage.ConfirmWrong); } } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort) { if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { updateStage(Stage.ChoiceTooShort); } else { mChosenPattern = new ArrayList(pattern); updateStage(Stage.FirstChoiceValid); } } else { throw new IllegalStateException("Unexpected stage " + mUiStage + " when " + "entering the pattern."); } } private void patternInProgress() { mHeaderText.setText(R.string.lockpattern_recording_inprogress); mFooterText.setText(""); mFooterLeftButton.setEnabled(false); mFooterRightButton.setEnabled(false); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mLockPatternUtils = new LockPatternUtils(this); requestWindowFeature(Window.FEATURE_NO_TITLE); setupViews(); // make it so unhandled touch events within the unlock screen go to the // lock pattern view. final LinearLayoutWithDefaultTouchRecepient topLayout = (LinearLayoutWithDefaultTouchRecepient) findViewById( R.id.topLayout); topLayout.setDefaultTouchRecepient(mLockPatternView); if (savedInstanceState == null) { // first launch updateStage(Stage.Introduction); if (mLockPatternUtils.savedPatternExists()) { confirmPattern(); } } else { // restore from previous state final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); if (patternString != null) { mChosenPattern = LockPatternUtils.stringToPattern(patternString); } updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); } } /** * Keep all "find view" related stuff confined to this function since in * case someone needs to subclass and customize. */ protected void setupViews() { setContentView(R.layout.choose_lock_pattern); mHeaderText = (TextView) findViewById(R.id.headerText); mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); mFooterText = (TextView) findViewById(R.id.footerText); mFooterLeftButton = (TextView) findViewById(R.id.footerLeftButton); mFooterRightButton = (TextView) findViewById(R.id.footerRightButton); mFooterLeftButton.setOnClickListener(this); mFooterRightButton.setOnClickListener(this); } public void onClick(View v) { if (v == mFooterLeftButton) { if (mUiStage.leftMode == LeftButtonMode.Retry) { mChosenPattern = null; mLockPatternView.clearPattern(); updateStage(Stage.Introduction); } else if (mUiStage.leftMode == LeftButtonMode.Cancel) { // They are canceling the entire wizard setResult(RESULT_FINISHED); finish(); } else { throw new IllegalStateException("left footer button pressed, but stage of " + mUiStage + " doesn't make sense"); } } else if (v == mFooterRightButton) { if (mUiStage.rightMode == RightButtonMode.Continue) { if (mUiStage != Stage.FirstChoiceValid) { throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid + " when button is " + RightButtonMode.Continue); } updateStage(Stage.NeedToConfirm); } else if (mUiStage.rightMode == RightButtonMode.Confirm) { if (mUiStage != Stage.ChoiceConfirmed) { throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed + " when button is " + RightButtonMode.Confirm); } saveChosenPatternAndFinish(); } else if (mUiStage.rightMode == RightButtonMode.Ok) { if (mUiStage != Stage.HelpScreen) { throw new IllegalStateException( "Help screen is only mode with ok button, but " + "stage is " + mUiStage); } mLockPatternView.clearPattern(); mLockPatternView.setDisplayMode(DisplayMode.Correct); updateStage(Stage.Introduction); } } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { if (mUiStage == Stage.HelpScreen) { updateStage(Stage.Introduction); return true; } } if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) { updateStage(Stage.HelpScreen); return true; } return super.onKeyDown(keyCode, event); } /** * Launch screen to confirm the existing lock pattern. * * @see #onActivityResult(int, int, android.content.Intent) */ protected void confirmPattern() { final Intent intent = new Intent(this, ConfirmLockPattern.class); //intent.setClassName("com.liato.bankdroid.lockpattern", "com.liato.bankdroid.lockpattern.ConfirmLockPattern"); startActivityForResult(intent, 55); } /** * @see #confirmPattern */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != 55) { return; } if (resultCode != Activity.RESULT_OK) { setResult(RESULT_FINISHED); finish(); } updateStage(Stage.Introduction); } @Override protected void onSaveInstanceState(Bundle outState) { outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); if (mChosenPattern != null) { outState.putString(KEY_PATTERN_CHOICE, LockPatternUtils.patternToString(mChosenPattern)); } super.onSaveInstanceState(outState); } /** * Updates the messages and buttons appropriate to what stage the user * is at in choosing a view. This doesn't handle clearing out the pattern; * the pattern is expected to be in the right state. */ protected void updateStage(Stage stage) { mUiStage = stage; // header text, footer text, visibility and // enabled state all known from the stage if (stage == Stage.ChoiceTooShort) { mHeaderText.setText( getResources().getString( stage.headerMessage, LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); } else { mHeaderText.setText(stage.headerMessage); } if (stage.footerMessage == ID_EMPTY_MESSAGE) { mFooterText.setText(""); } else { mFooterText.setText(stage.footerMessage); } if (stage.leftMode == LeftButtonMode.Gone) { mFooterLeftButton.setVisibility(View.GONE); } else { mFooterLeftButton.setVisibility(View.VISIBLE); mFooterLeftButton.setText(stage.leftMode.text); mFooterLeftButton.setEnabled(stage.leftMode.enabled); } mFooterRightButton.setText(stage.rightMode.text); mFooterRightButton.setEnabled(stage.rightMode.enabled); // same for whether the patten is enabled if (stage.patternEnabled) { mLockPatternView.enableInput(); } else { mLockPatternView.disableInput(); } // the rest of the stuff varies enough that it is easier just to handle // on a case by case basis. mLockPatternView.setDisplayMode(DisplayMode.Correct); switch (mUiStage) { case Introduction: mLockPatternView.clearPattern(); break; case HelpScreen: mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); break; case ChoiceTooShort: mLockPatternView.setDisplayMode(DisplayMode.Wrong); postClearPatternRunnable(); break; case FirstChoiceValid: break; case NeedToConfirm: mLockPatternView.clearPattern(); break; case ConfirmWrong: mLockPatternView.setDisplayMode(DisplayMode.Wrong); postClearPatternRunnable(); break; case ChoiceConfirmed: break; } } // clear the wrong pattern unless they have started a new one // already private void postClearPatternRunnable() { mLockPatternView.removeCallbacks(mClearPatternRunnable); mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); } private void saveChosenPatternAndFinish() { final boolean lockVirgin = !mLockPatternUtils.isPatternEverChosen(); mLockPatternUtils.saveLockPattern(mChosenPattern); mLockPatternUtils.setLockPatternEnabled(true); if (lockVirgin) { mLockPatternUtils.setVisiblePatternEnabled(true); mLockPatternUtils.setTactileFeedbackEnabled(false); } setResult(RESULT_FINISHED); finish(); } /** * The states of the left footer button. */ enum LeftButtonMode { Cancel(android.R.string.cancel, true), CancelDisabled(android.R.string.cancel, false), Retry(R.string.lockpattern_retry_button_text, true), RetryDisabled(R.string.lockpattern_retry_button_text, false), Gone(ID_EMPTY_MESSAGE, false); final int text; final boolean enabled; /** * @param text The displayed text for this mode. * @param enabled Whether the button should be enabled. */ LeftButtonMode(int text, boolean enabled) { this.text = text; this.enabled = enabled; } } /** * The states of the right button. */ enum RightButtonMode { Continue(R.string.lockpattern_continue_button_text, true), ContinueDisabled(R.string.lockpattern_continue_button_text, false), Confirm(R.string.lockpattern_confirm_button_text, true), ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), Ok(android.R.string.ok, true); final int text; final boolean enabled; /** * @param text The displayed text for this mode. * @param enabled Whether the button should be enabled. */ RightButtonMode(int text, boolean enabled) { this.text = text; this.enabled = enabled; } } /** * Keep track internally of where the user is in choosing a pattern. */ protected enum Stage { Introduction( R.string.lockpattern_recording_intro_header, LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, R.string.lockpattern_recording_intro_footer, true), HelpScreen( R.string.lockpattern_settings_help_how_to_record, LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), ChoiceTooShort( R.string.lockpattern_recording_incorrect_too_short, LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, ID_EMPTY_MESSAGE, true), FirstChoiceValid( R.string.lockpattern_pattern_entered_header, LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), NeedToConfirm( R.string.lockpattern_need_to_confirm, LeftButtonMode.CancelDisabled, RightButtonMode.ConfirmDisabled, ID_EMPTY_MESSAGE, true), ConfirmWrong( R.string.lockpattern_need_to_unlock_wrong, LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, ID_EMPTY_MESSAGE, true), ChoiceConfirmed( R.string.lockpattern_pattern_confirmed_header, LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false); final int headerMessage; final LeftButtonMode leftMode; final RightButtonMode rightMode; final int footerMessage; final boolean patternEnabled; /** * @param headerMessage The message displayed at the top. * @param leftMode The mode of the left button. * @param rightMode The mode of the right button. * @param footerMessage The footer message. * @param patternEnabled Whether the pattern widget is enabled. */ Stage(int headerMessage, LeftButtonMode leftMode, RightButtonMode rightMode, int footerMessage, boolean patternEnabled) { this.headerMessage = headerMessage; this.leftMode = leftMode; this.rightMode = rightMode; this.footerMessage = footerMessage; this.patternEnabled = patternEnabled; } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternExample.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.liato.bankdroid.R; import android.app.Activity; import android.content.Intent; import android.graphics.drawable.AnimationDrawable; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.ImageView; public class ChooseLockPatternExample extends Activity implements View.OnClickListener { private static final int REQUESTCODE_CHOOSE = 1; private static final long START_DELAY = 1000; private View mNextButton; private View mSkipButton; private AnimationDrawable mAnimation; private Runnable mRunnable = new Runnable() { public void run() { startAnimation(mAnimation); } }; private Handler mHandler = new Handler(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.choose_lock_pattern_example); initViews(); } @Override protected void onResume() { super.onResume(); mHandler.postDelayed(mRunnable, START_DELAY); } @Override protected void onPause() { stopAnimation(mAnimation); super.onPause(); } public void onClick(View v) { if (v == mSkipButton) { // Canceling, so finish all setResult(ChooseLockPattern.RESULT_FINISHED); finish(); } else if (v == mNextButton) { stopAnimation(mAnimation); Intent intent = new Intent(this, ChooseLockPattern.class); startActivityForResult(intent, REQUESTCODE_CHOOSE); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUESTCODE_CHOOSE && resultCode == ChooseLockPattern.RESULT_FINISHED) { setResult(resultCode); finish(); } } private void initViews() { mNextButton = findViewById(R.id.next_button); mNextButton.setOnClickListener(this); mSkipButton = findViewById(R.id.skip_button); mSkipButton.setOnClickListener(this); View imageView = (ImageView) findViewById(R.id.lock_anim); imageView.setBackgroundResource(R.drawable.lock_anim); imageView.setOnClickListener(this); mAnimation = (AnimationDrawable) imageView.getBackground(); } protected void startAnimation(final AnimationDrawable animation) { if (animation != null && !animation.isRunning()) { animation.run(); } } protected void stopAnimation(final AnimationDrawable animation) { if (animation != null && animation.isRunning()) { animation.stop(); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternTutorial.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.liato.bankdroid.R; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; public class ChooseLockPatternTutorial extends Activity implements View.OnClickListener { private static final int REQUESTCODE_EXAMPLE = 1; private View mNextButton; private View mSkipButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Don't show the tutorial if the user has seen it before. LockPatternUtils lockPatternUtils = new LockPatternUtils(this); if (savedInstanceState == null && lockPatternUtils.isPatternEverChosen()) { Intent intent = new Intent(); intent.setClassName("com.android.settings", "com.android.settings.ChooseLockPattern"); startActivity(intent); finish(); } else { initViews(); } } private void initViews() { setContentView(R.layout.choose_lock_pattern_tutorial); mNextButton = findViewById(R.id.next_button); mNextButton.setOnClickListener(this); mSkipButton = findViewById(R.id.skip_button); mSkipButton.setOnClickListener(this); } public void onClick(View v) { if (v == mSkipButton) { // Canceling, so finish all setResult(ChooseLockPattern.RESULT_FINISHED); finish(); } else if (v == mNextButton) { startActivityForResult(new Intent(this, ChooseLockPatternExample.class), REQUESTCODE_EXAMPLE); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUESTCODE_EXAMPLE && resultCode == ChooseLockPattern.RESULT_FINISHED) { setResult(resultCode); finish(); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/ConfirmLockPattern.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.liato.bankdroid.Helpers; import com.liato.bankdroid.R; import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.CountDownTimer; import android.os.SystemClock; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import java.util.List; /** * Launch this when you want the user to confirm their lock pattern. * * Sets an activity result of {@link Activity#RESULT_OK} when the user * successfully confirmed their pattern. */ public class ConfirmLockPattern extends Activity { /** * Names of {@link CharSequence} fields within the originating {@link Intent} * that are used to configure the keyguard confirmation view's labeling. * The view will use the system-defined resource strings for any labels that * the caller does not supply. */ public static final String HEADER_TEXT = "com.liato.bankdroid.header"; public static final String FOOTER_TEXT = "com.liato.bankdroid.footer"; public static final String HEADER_WRONG_TEXT = "com.liato.bankdroid.header_wrong"; public static final String FOOTER_WRONG_TEXT = "com.liato.bankdroid.footer_wrong"; // how long we wait to clear a wrong pattern private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts"; private LockPatternView mLockPatternView; private LockPatternUtils mLockPatternUtils; private int mNumWrongConfirmAttempts; private CountDownTimer mCountdownTimer; private TextView mHeaderTextView; private TextView mFooterTextView; // caller-supplied text for various prompts private CharSequence mHeaderText; private CharSequence mFooterText; private CharSequence mHeaderWrongText; private CharSequence mFooterWrongText; private Runnable mClearPatternRunnable = new Runnable() { public void run() { mLockPatternView.clearPattern(); } }; /** * The pattern listener that responds according to a user confirming * an existing lock pattern. */ private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener = new LockPatternView.OnPatternListener() { public void onPatternStart() { mLockPatternView.removeCallbacks(mClearPatternRunnable); } public void onPatternCleared() { mLockPatternView.removeCallbacks(mClearPatternRunnable); } public void onPatternDetected(List pattern) { if (mLockPatternUtils.checkPattern(pattern)) { setResult(RESULT_OK); finish(); } else { if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL && ++mNumWrongConfirmAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); handleAttemptLockout(deadline); } else { updateStage(Stage.NeedToUnlockWrong); postClearPatternRunnable(); } } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mLockPatternUtils = new LockPatternUtils(this); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.confirm_lock_pattern); mHeaderTextView = (TextView) findViewById(R.id.headerText); mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); mFooterTextView = (TextView) findViewById(R.id.footerText); // make it so unhandled touch events within the unlock screen go to the // lock pattern view. final LinearLayoutWithDefaultTouchRecepient topLayout = (LinearLayoutWithDefaultTouchRecepient) findViewById( R.id.topLayout); topLayout.setDefaultTouchRecepient(mLockPatternView); Intent intent = getIntent(); if (intent != null) { mHeaderText = intent.getCharSequenceExtra(HEADER_TEXT); mFooterText = intent.getCharSequenceExtra(FOOTER_TEXT); mHeaderWrongText = intent.getCharSequenceExtra(HEADER_WRONG_TEXT); mFooterWrongText = intent.getCharSequenceExtra(FOOTER_WRONG_TEXT); } mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener); updateStage(Stage.NeedToUnlock); if (savedInstanceState != null) { mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS); } else { // on first launch, if no lock pattern is set, then finish with // success (don't want user to get stuck confirming something that // doesn't exist). if (!mLockPatternUtils.savedPatternExists()) { setResult(RESULT_OK); finish(); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override protected void onSaveInstanceState(Bundle outState) { // deliberately not calling super since we are managing this in full outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts); super.onSaveInstanceState(outState); } @Override protected void onPause() { if (mCountdownTimer != null) { mCountdownTimer.cancel(); } super.onPause(); } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { Intent homeIntent = new Intent(Intent.ACTION_MAIN); homeIntent.addCategory(Intent.CATEGORY_HOME); homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(homeIntent); return true; } return super.onKeyDown(keyCode, event); } @Override protected void onResume() { super.onResume(); // if the user is currently locked out, enforce it. long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); if (deadline != 0) { handleAttemptLockout(deadline); } } private void updateStage(Stage stage) { switch (stage) { case NeedToUnlock: if (mHeaderText != null) { mHeaderTextView.setText(mHeaderText); } else { mHeaderTextView.setText(R.string.lockpattern_need_to_unlock); } if (mFooterText != null) { mFooterTextView.setText(mFooterText); } else { mFooterTextView.setText(R.string.lockpattern_need_to_unlock_footer); } mLockPatternView.setEnabled(true); mLockPatternView.enableInput(); break; case NeedToUnlockWrong: if (mHeaderWrongText != null) { mHeaderTextView.setText(mHeaderWrongText); } else { mHeaderTextView.setText(R.string.lockpattern_need_to_unlock_wrong); } if (mFooterWrongText != null) { mFooterTextView.setText(mFooterWrongText); } else { mFooterTextView.setText(R.string.lockpattern_need_to_unlock_wrong_footer); } mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.setEnabled(true); mLockPatternView.enableInput(); break; case LockedOut: mLockPatternView.clearPattern(); // enabled = false means: disable input, and have the // appearance of being disabled. mLockPatternView.setEnabled(false); // appearance of being disabled break; } } // clear the wrong pattern unless they have started a new one // already private void postClearPatternRunnable() { mLockPatternView.removeCallbacks(mClearPatternRunnable); mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); } private void handleAttemptLockout(long elapsedRealtimeDeadline) { updateStage(Stage.LockedOut); long elapsedRealtime = SystemClock.elapsedRealtime(); mCountdownTimer = new CountDownTimer( elapsedRealtimeDeadline - elapsedRealtime, LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) { @Override public void onTick(long millisUntilFinished) { mHeaderTextView .setText(R.string.lockpattern_too_many_failed_confirmation_attempts_header); final int secondsCountdown = (int) (millisUntilFinished / 1000); mFooterTextView.setText(getString( R.string.lockpattern_too_many_failed_confirmation_attempts_footer, secondsCountdown)); } @Override public void onFinish() { mNumWrongConfirmAttempts = 0; updateStage(Stage.NeedToUnlock); } }.start(); } @Override public void finish() { Helpers.setActivityAnimation(this, R.anim.zoom_enter, R.anim.zoom_exit); super.finish(); } private enum Stage { NeedToUnlock, NeedToUnlockWrong, LockedOut } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/LinearLayoutWithDefaultTouchRecepient.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; /** * Like a normal linear layout, but supports dispatching all otherwise unhandled * touch events to a particular descendant. This is for the unlock screen, so * that a wider range of touch events than just the lock pattern widget can kick * off a lock pattern if the finger is eventually dragged into the bounds of the * lock pattern view. */ public class LinearLayoutWithDefaultTouchRecepient extends LinearLayout { private final Rect mTempRect = new Rect(); private View mDefaultTouchRecepient; public LinearLayoutWithDefaultTouchRecepient(Context context) { super(context); } public LinearLayoutWithDefaultTouchRecepient(Context context, AttributeSet attrs) { super(context, attrs); } public void setDefaultTouchRecepient(View defaultTouchRecepient) { mDefaultTouchRecepient = defaultTouchRecepient; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mDefaultTouchRecepient == null) { return super.dispatchTouchEvent(ev); } if (super.dispatchTouchEvent(ev)) { return true; } mTempRect.set(0, 0, 0, 0); offsetRectIntoDescendantCoords(mDefaultTouchRecepient, mTempRect); ev.setLocation(ev.getX() + mTempRect.left, ev.getY() + mTempRect.top); return mDefaultTouchRecepient.dispatchTouchEvent(ev); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/LockPatternUtils.java ================================================ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.google.common.collect.Lists; import com.liato.bankdroid.utils.StringUtils; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.Settings; import android.text.TextUtils; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; import timber.log.Timber; /** * Utilities for the lock patten and its settings. */ public class LockPatternUtils { /** * The maximum number of incorrect attempts before the user is prevented * from trying again for {@link #FAILED_ATTEMPT_TIMEOUT_MS}. */ public static final int FAILED_ATTEMPTS_BEFORE_TIMEOUT = 5; /** * The number of incorrect attempts before which we fall back on an alternative * method of verifying the user, and resetting their lock pattern. */ public static final int FAILED_ATTEMPTS_BEFORE_RESET = 20; /** * How long the user is prevented from trying again after entering the * wrong pattern too many times. */ public static final long FAILED_ATTEMPT_TIMEOUT_MS = 30000L; /** * The interval of the countdown for showing progress of the lockout. */ public static final long FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS = 1000L; /** * The minimum number of dots in a valid pattern. */ public static final int MIN_LOCK_PATTERN_SIZE = 4; /** * The minimum number of dots the user must include in a wrong pattern * attempt for it to be counted against the counts that affect * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET} */ public static final int MIN_PATTERN_REGISTER_FAIL = 3; private static final String LOCK_PATTERN_FILE = "gesture.key"; private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; private final static String PATTERN_EVER_CHOSEN = "lockscreen.patterneverchosen"; private static String sLockPatternFilename; private static SharedPreferences mPrefs; private final ContentResolver mContentResolver; public LockPatternUtils(Context context) { mContentResolver = context.getContentResolver(); mPrefs = PreferenceManager.getDefaultSharedPreferences(context); // Initialize the location of gesture lock file if (sLockPatternFilename == null) { sLockPatternFilename = context.getFilesDir() + LOCK_PATTERN_FILE; //sLockPatternFilename = android.os.Environment.getDataDirectory() // .getAbsolutePath() + LOCK_PATTERN_FILE; } } /** * Deserialize a pattern. * * @param string The pattern serialized with {@link #patternToString} * @return The pattern. */ public static List stringToPattern(String string) { List result = Lists.newArrayList(); final byte[] bytes = StringUtils.getBytes(string); for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; result.add(LockPatternView.Cell.of(b / 3, b % 3)); } return result; } /** * Serialize a pattern. * * @param pattern The pattern. * @return The pattern in string form. */ public static String patternToString(List pattern) { if (pattern == null) { return ""; } final int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { LockPatternView.Cell cell = pattern.get(i); res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); } return StringUtils.toString(res); } /* * Generate an SHA-1 hash for the pattern. Not the most secure, but it is * at least a second level of protection. First level is that the file * is in a location only readable by the system process. * @param pattern the gesture pattern. * @return the hash of the pattern in a byte array. */ static byte[] patternToHash(List pattern) { if (pattern == null) { return null; } final int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { LockPatternView.Cell cell = pattern.get(i); res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); } try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] hash = md.digest(res); return hash; } catch (NoSuchAlgorithmException nsa) { return res; } } /** * Check to see if a pattern matches the saved pattern. If no pattern exists, * always returns true. * * @param pattern The pattern to check. * @return Whether the pattern matchees the stored one. */ public boolean checkPattern(List pattern) { try { // Read all the bytes from the file RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r"); final byte[] stored = new byte[(int) raf.length()]; int got = raf.read(stored, 0, stored.length); raf.close(); if (got <= 0) { return true; } // Compare the hash from the file with the entered pattern's hash return Arrays.equals(stored, LockPatternUtils.patternToHash(pattern)); } catch (FileNotFoundException fnfe) { return true; } catch (IOException ioe) { return true; } } /** * Check to see if the user has stored a lock pattern. * * @return Whether a saved pattern exists. */ public boolean savedPatternExists() { try { // Check if we can read a byte from the file RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r"); byte first = raf.readByte(); raf.close(); return true; } catch (FileNotFoundException fnfe) { return false; } catch (IOException ioe) { return false; } } /** * Return true if the user has ever chosen a pattern. This is true even if the pattern is * currently cleared. * * @return True if the user has ever chosen a pattern. */ public boolean isPatternEverChosen() { return getBoolean(PATTERN_EVER_CHOSEN); } /** * Save a lock pattern. * * @param pattern The new pattern to save. */ public void saveLockPattern(List pattern) { if (pattern == null) { Timber.d("Removing lock pattern"); } else { Timber.v("Saving lock pattern: %s", LockPatternUtils.patternToString(pattern)); } // Compute the hash final byte[] hash = LockPatternUtils.patternToHash(pattern); try { // Write the hash to file RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "rw"); // Truncate the file if pattern is null, to clear the lock if (pattern == null) { raf.setLength(0); } else { raf.write(hash, 0, hash.length); } raf.close(); setBoolean(PATTERN_EVER_CHOSEN, true); } catch (FileNotFoundException fnfe) { // Cant do much, unless we want to fail over to using the settings provider Timber.e(fnfe, "Unable to save lock pattern to %s", sLockPatternFilename); } catch (IOException ioe) { // Cant do much Timber.e(ioe, "Unable to save lock pattern to %s", sLockPatternFilename); } } /** * @return Whether the lock pattern is enabled. */ public boolean isLockPatternEnabled() { return getBoolean(Settings.System.LOCK_PATTERN_ENABLED); } /** * Set whether the lock pattern is enabled. */ public void setLockPatternEnabled(boolean enabled) { setBoolean(Settings.System.LOCK_PATTERN_ENABLED, enabled); } /** * @return Whether the visible pattern is enabled. */ public boolean isVisiblePatternEnabled() { return getBoolean(Settings.System.LOCK_PATTERN_VISIBLE); } /** * Set whether the visible pattern is enabled. */ public void setVisiblePatternEnabled(boolean enabled) { setBoolean(Settings.System.LOCK_PATTERN_VISIBLE, enabled); } /** * @return Whether tactile feedback for the pattern is enabled. */ public boolean isTactileFeedbackEnabled() { return getBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED); } /** * Set whether tactile feedback for the pattern is enabled. */ public void setTactileFeedbackEnabled(boolean enabled) { setBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED, enabled); } /** * Set and store the lockout deadline, meaning the user can't attempt his/her unlock * pattern until the deadline has passed. * * @return the chosen deadline. */ public long setLockoutAttemptDeadline() { final long deadline = SystemClock.elapsedRealtime() + FAILED_ATTEMPT_TIMEOUT_MS; setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline); return deadline; } /** * @return The elapsed time in millis in the future when the user is allowed to * attempt to enter his/her lock pattern, or 0 if the user is welcome to * enter a pattern. */ public long getLockoutAttemptDeadline() { final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L); final long now = SystemClock.elapsedRealtime(); if (deadline < now || deadline > (now + FAILED_ATTEMPT_TIMEOUT_MS)) { return 0L; } return deadline; } /** * @return Whether the user is permanently locked out until they verify their * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed * attempts. */ public boolean isPermanentlyLocked() { return getBoolean(LOCKOUT_PERMANENT_KEY); } /** * Set the state of whether the device is permanently locked, meaning the user * must authenticate via other means. If false, that means the user has gone * out of permanent lock, so the existing (forgotten) lock pattern needs to * be cleared. * * @param locked Whether the user is permanently locked out until they verify their * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed * attempts. */ public void setPermanentlyLocked(boolean locked) { setBoolean(LOCKOUT_PERMANENT_KEY, locked); if (!locked) { setLockPatternEnabled(false); saveLockPattern(null); } } /** * @return A formatted string of the next alarm (for showing on the lock screen), * or null if there is no next alarm. */ public String getNextAlarm() { String nextAlarm = Settings.System.getString(mContentResolver, Settings.System.NEXT_ALARM_FORMATTED); if (nextAlarm == null || TextUtils.isEmpty(nextAlarm)) { return null; } return nextAlarm; } private boolean getBoolean(String systemSettingKey) { return mPrefs.getBoolean(systemSettingKey, false); } private void setBoolean(String systemSettingKey, boolean enabled) { Editor editor = mPrefs.edit(); editor.putBoolean(systemSettingKey, enabled); editor.apply(); } private long getLong(String systemSettingKey, long def) { return mPrefs.getLong(systemSettingKey, def); } private void setLong(String systemSettingKey, long value) { Editor editor = mPrefs.edit(); editor.putLong(systemSettingKey, value); editor.apply(); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/lockpattern/LockPatternView.java ================================================ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.lockpattern; import com.liato.bankdroid.R; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.os.Debug; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.os.Vibrator; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; /** * Displays and detects the user's unlock attempt, which is a drag of a finger * across 9 regions of the screen. * * Is also capable of displaying a static pattern in "in progress", "wrong" or * "correct" states. */ public class LockPatternView extends View { // TODO: make this common with PhoneWindow static final int STATUS_BAR_HEIGHT = 25; // Vibrator pattern for creating a tactile bump private static final long[] VIBE_PATTERN = {0, 1, 40, 41}; private static final boolean PROFILE_DRAWING = false; /** * How many milliseconds we spend animating each circle of a lock pattern * if the animating mode is set. The entire animation should take this * constant * the length of the pattern to complete. */ private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; private final Path mCurrentPath = new Path(); private final Rect mInvalidate = new Rect(); protected int mPaddingLeft; protected int mPaddingRight; protected int mPaddingTop; protected int mPaddingBottom; private boolean mDrawingProfilingStarted = false; private Paint mPaint = new Paint(); private Paint mPathPaint = new Paint(); private OnPatternListener mOnPatternListener; private ArrayList mPattern = new ArrayList(9); /** * Lookup table for the circles of the pattern we are currently drawing. * This will be the cells of the complete pattern unless we are animating, * in which case we use this to hold the cells we are drawing for the in * progress animation. */ private boolean[][] mPatternDrawLookup = new boolean[3][3]; /** * the in progress point: * - during interaction: where the user's finger is * - during animation: the current tip of the animating line */ private float mInProgressX = -1; private float mInProgressY = -1; private long mAnimatingPeriodStart; private DisplayMode mPatternDisplayMode = DisplayMode.Correct; private boolean mInputEnabled = true; private boolean mInStealthMode = false; private boolean mTactileFeedbackEnabled = true; private boolean mPatternInProgress = false; private float mDiameterFactor = 0.5f; private float mHitFactor = 0.6f; private float mSquareWidth; private float mSquareHeight; private Bitmap mBitmapBtnDefault; private Bitmap mBitmapBtnTouched; private Bitmap mBitmapCircleDefault; private Bitmap mBitmapCircleGreen; private Bitmap mBitmapCircleRed; private Bitmap mBitmapArrowGreenUp; private Bitmap mBitmapArrowRedUp; private int mBitmapWidth; private int mBitmapHeight; private Vibrator vibe; // Vibrator for creating tactile feedback public LockPatternView(Context context) { this(context, null); } public LockPatternView(Context context, AttributeSet attrs) { super(context, attrs); //vibe = new Vibrator(); vibe = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); setClickable(true); mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); mPathPaint.setColor(Color.WHITE); // TODO this should be from the style mPathPaint.setAlpha(128); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); // lot's of bitmaps! mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default); mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched); mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default); mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green); mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red); mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up); mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up); // we assume all bitmaps have the same size mBitmapWidth = mBitmapBtnDefault.getWidth(); mBitmapHeight = mBitmapBtnDefault.getHeight(); } private Bitmap getBitmapFor(int resId) { return BitmapFactory.decodeResource(getContext().getResources(), resId); } /** * @return Whether the view is in stealth mode. */ public boolean isInStealthMode() { return mInStealthMode; } /** * Set whether the view is in stealth mode. If true, there will be no * visible feedback as the user enters the pattern. * * @param inStealthMode Whether in stealth mode. */ public void setInStealthMode(boolean inStealthMode) { mInStealthMode = inStealthMode; } /** * @return Whether the view has tactile feedback enabled. */ public boolean isTactileFeedbackEnabled() { return mTactileFeedbackEnabled; } /** * Set whether the view will use tactile feedback. If true, there will be * tactile feedback as the user enters the pattern. * * @param tactileFeedbackEnabled Whether tactile feedback is enabled */ public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { mTactileFeedbackEnabled = tactileFeedbackEnabled; } /** * Set the call back for pattern detection. * * @param onPatternListener The call back. */ public void setOnPatternListener( OnPatternListener onPatternListener) { mOnPatternListener = onPatternListener; } /** * Set the pattern explicitely (rather than waiting for the user to input * a pattern). * * @param displayMode How to display the pattern. * @param pattern The pattern. */ public void setPattern(DisplayMode displayMode, List pattern) { mPattern.clear(); mPattern.addAll(pattern); clearPatternDrawLookup(); for (Cell cell : pattern) { mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; } setDisplayMode(displayMode); } /** * Set the display mode of the current pattern. This can be useful, for * instance, after detecting a pattern to tell this view whether change the * in progress result to correct or wrong. * * @param displayMode The display mode. */ public void setDisplayMode(DisplayMode displayMode) { mPatternDisplayMode = displayMode; if (displayMode == DisplayMode.Animate) { if (mPattern.size() == 0) { throw new IllegalStateException("you must have a pattern to " + "animate if you want to set the display mode to animate"); } mAnimatingPeriodStart = SystemClock.elapsedRealtime(); final Cell first = mPattern.get(0); mInProgressX = getCenterXForColumn(first.getColumn()); mInProgressY = getCenterYForRow(first.getRow()); clearPatternDrawLookup(); } invalidate(); } /** * Clear the pattern. */ public void clearPattern() { resetPattern(); } /** * Reset all pattern state. */ private void resetPattern() { mPattern.clear(); clearPatternDrawLookup(); mPatternDisplayMode = DisplayMode.Correct; invalidate(); } /** * Clear the pattern lookup table. */ private void clearPatternDrawLookup() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { mPatternDrawLookup[i][j] = false; } } } /** * Disable input (for instance when displaying a message that will * timeout so user doesn't get view into messy state). */ public void disableInput() { mInputEnabled = false; } /** * Enable input. */ public void enableInput() { mInputEnabled = true; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { final int width = w - mPaddingLeft - mPaddingRight; mSquareWidth = width / 3.0f; final int height = h - mPaddingTop - mPaddingBottom; mSquareHeight = height / 3.0f; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); final int height = MeasureSpec.getSize(heightMeasureSpec); final int squareSide = Math.min(width, height); setMeasuredDimension(squareSide, squareSide); } /** * Determines whether the point x, y will add a new point to the current * pattern (in addition to finding the cell, also makes heuristic choices * such as filling in gaps based on current pattern). * * @param x The x coordinate. * @param y The y coordinate. */ private Cell detectAndAddHit(float x, float y) { final Cell cell = checkForNewHit(x, y); if (cell != null) { // check for gaps in existing pattern Cell fillInGapCell = null; final ArrayList pattern = mPattern; if (!pattern.isEmpty()) { final Cell lastCell = pattern.get(pattern.size() - 1); int dRow = cell.row - lastCell.row; int dColumn = cell.column - lastCell.column; int fillInRow = lastCell.row; int fillInColumn = lastCell.column; if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); } if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); } fillInGapCell = Cell.of(fillInRow, fillInColumn); } if (fillInGapCell != null && !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { addCellToPattern(fillInGapCell); } addCellToPattern(cell); if (mTactileFeedbackEnabled) { vibe.vibrate(VIBE_PATTERN, -1); // Generate tactile feedback } return cell; } return null; } private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; mPattern.add(newCell); } // helper method to find which cell a point maps to private Cell checkForNewHit(float x, float y) { final int rowHit = getRowHit(y); if (rowHit < 0) { return null; } final int columnHit = getColumnHit(x); if (columnHit < 0) { return null; } if (mPatternDrawLookup[rowHit][columnHit]) { return null; } return Cell.of(rowHit, columnHit); } /** * Helper method to find the row that y falls into. * * @param y The y coordinate * @return The row that y falls in, or -1 if it falls in no row. */ private int getRowHit(float y) { final float squareHeight = mSquareHeight; float hitSize = squareHeight * mHitFactor; float offset = mPaddingTop + (squareHeight - hitSize) / 2f; for (int i = 0; i < 3; i++) { final float hitTop = offset + squareHeight * i; if (y >= hitTop && y <= hitTop + hitSize) { return i; } } return -1; } /** * Helper method to find the column x fallis into. * * @param x The x coordinate. * @return The column that x falls in, or -1 if it falls in no column. */ private int getColumnHit(float x) { final float squareWidth = mSquareWidth; float hitSize = squareWidth * mHitFactor; float offset = mPaddingLeft + (squareWidth - hitSize) / 2f; for (int i = 0; i < 3; i++) { final float hitLeft = offset + squareWidth * i; if (x >= hitLeft && x <= hitLeft + hitSize) { return i; } } return -1; } @Override public boolean onTouchEvent(MotionEvent motionEvent) { if (!mInputEnabled || !isEnabled()) { return false; } final float x = motionEvent.getX(); final float y = motionEvent.getY(); Cell hitCell; switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: resetPattern(); hitCell = detectAndAddHit(x, y); if (hitCell != null && mOnPatternListener != null) { mPatternInProgress = true; mPatternDisplayMode = DisplayMode.Correct; mOnPatternListener.onPatternStart(); } else if (mOnPatternListener != null) { mPatternInProgress = false; mOnPatternListener.onPatternCleared(); } if (hitCell != null) { final float startX = getCenterXForColumn(hitCell.column); final float startY = getCenterYForRow(hitCell.row); final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), (int) (startX + widthOffset), (int) (startY + heightOffset)); } mInProgressX = x; mInProgressY = y; if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } return true; case MotionEvent.ACTION_UP: // report pattern detected if (!mPattern.isEmpty() && mOnPatternListener != null) { mPatternInProgress = false; mOnPatternListener.onPatternDetected(mPattern); invalidate(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } return true; case MotionEvent.ACTION_MOVE: final int patternSizePreHitDetect = mPattern.size(); hitCell = detectAndAddHit(x, y); final int patternSize = mPattern.size(); if (hitCell != null && (mOnPatternListener != null) && (patternSize == 1)) { mPatternInProgress = true; mOnPatternListener.onPatternStart(); } // note current x and y for rubber banding of in progress // patterns final float dx = Math.abs(x - mInProgressX); final float dy = Math.abs(y - mInProgressY); if (dx + dy > mSquareWidth * 0.01f) { float oldX = mInProgressX; float oldY = mInProgressY; mInProgressX = x; mInProgressY = y; if (mPatternInProgress && patternSize > 0) { final ArrayList pattern = mPattern; final float radius = mSquareWidth * mDiameterFactor * 0.5f; final Cell lastCell = pattern.get(patternSize - 1); float startX = getCenterXForColumn(lastCell.column); float startY = getCenterYForRow(lastCell.row); float left; float top; float right; float bottom; final Rect invalidateRect = mInvalidate; if (startX < x) { left = startX; right = x; } else { left = x; right = startX; } if (startY < y) { top = startY; bottom = y; } else { top = y; bottom = startY; } // Invalidate between the pattern's last cell and the current location invalidateRect.set((int) (left - radius), (int) (top - radius), (int) (right + radius), (int) (bottom + radius)); if (startX < oldX) { left = startX; right = oldX; } else { left = oldX; right = startX; } if (startY < oldY) { top = startY; bottom = oldY; } else { top = oldY; bottom = startY; } // Invalidate between the pattern's last cell and the previous location invalidateRect.union((int) (left - radius), (int) (top - radius), (int) (right + radius), (int) (bottom + radius)); // Invalidate between the pattern's new cell and the pattern's previous cell if (hitCell != null) { startX = getCenterXForColumn(hitCell.column); startY = getCenterYForRow(hitCell.row); if (patternSize >= 2) { // (re-using hitcell for old cell) hitCell = pattern.get(patternSize - 1 - (patternSize - patternSizePreHitDetect)); oldX = getCenterXForColumn(hitCell.column); oldY = getCenterYForRow(hitCell.row); if (startX < oldX) { left = startX; right = oldX; } else { left = oldX; right = startX; } if (startY < oldY) { top = startY; bottom = oldY; } else { top = oldY; bottom = startY; } } else { left = startX; right = startX; top = startY; bottom = startY; } final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidateRect.set((int) (left - widthOffset), (int) (top - heightOffset), (int) (right + widthOffset), (int) (bottom + heightOffset)); } invalidate(invalidateRect); } else { invalidate(); } } return true; case MotionEvent.ACTION_CANCEL: resetPattern(); if (mOnPatternListener != null) { mPatternInProgress = false; mOnPatternListener.onPatternCleared(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } return true; } return false; } private float getCenterXForColumn(int column) { return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; } private float getCenterYForRow(int row) { return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; } @Override protected void onDraw(Canvas canvas) { final ArrayList pattern = mPattern; final int count = pattern.size(); final boolean[][] drawLookup = mPatternDrawLookup; if (mPatternDisplayMode == DisplayMode.Animate) { // figure out which circles to draw // + 1 so we pause on complete pattern final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; final int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart) % oneCycle; final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; clearPatternDrawLookup(); for (int i = 0; i < numCircles; i++) { final Cell cell = pattern.get(i); drawLookup[cell.getRow()][cell.getColumn()] = true; } // figure out in progress portion of ghosting line final boolean needToUpdateInProgressPoint = numCircles > 0 && numCircles < count; if (needToUpdateInProgressPoint) { final float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / MILLIS_PER_CIRCLE_ANIMATING; final Cell currentCell = pattern.get(numCircles - 1); final float centerX = getCenterXForColumn(currentCell.column); final float centerY = getCenterYForRow(currentCell.row); final Cell nextCell = pattern.get(numCircles); final float dx = percentageOfNextCircle * (getCenterXForColumn(nextCell.column) - centerX); final float dy = percentageOfNextCircle * (getCenterYForRow(nextCell.row) - centerY); mInProgressX = centerX + dx; mInProgressY = centerY + dy; } // TODO: Infinite loop here... invalidate(); } final float squareWidth = mSquareWidth; final float squareHeight = mSquareHeight; float radius = (squareWidth * mDiameterFactor * 0.5f); mPathPaint.setStrokeWidth(radius); final Path currentPath = mCurrentPath; currentPath.rewind(); // TODO: the path should be created and cached every time we hit-detect a cell // only the last segment of the path should be computed here // draw the path of the pattern (unless the user is in progress, and // we are in stealth mode) final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong); if (drawPath) { boolean anyCircles = false; for (int i = 0; i < count; i++) { Cell cell = pattern.get(i); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[cell.row][cell.column]) { break; } anyCircles = true; float centerX = getCenterXForColumn(cell.column); float centerY = getCenterYForRow(cell.row); if (i == 0) { currentPath.moveTo(centerX, centerY); } else { currentPath.lineTo(centerX, centerY); } } // add last in progress section if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) && anyCircles) { currentPath.lineTo(mInProgressX, mInProgressY); } canvas.drawPath(currentPath, mPathPaint); } // draw the circles final int paddingTop = mPaddingTop; final int paddingLeft = mPaddingLeft; for (int i = 0; i < 3; i++) { float topY = paddingTop + i * squareHeight; //float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight / 2); for (int j = 0; j < 3; j++) { float leftX = paddingLeft + j * squareWidth; drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]); } } // draw the arrows associated with the path (unless the user is in progress, and // we are in stealth mode) boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0; mPaint.setFilterBitmap(true); // draw with higher quality since we render with transforms if (drawPath) { for (int i = 0; i < count - 1; i++) { Cell cell = pattern.get(i); Cell next = pattern.get(i + 1); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[next.row][next.column]) { break; } float leftX = paddingLeft + cell.column * squareWidth; float topY = paddingTop + cell.row * squareHeight; drawArrow(canvas, leftX, topY, cell, next); } } mPaint.setFilterBitmap(oldFlag); // restore default flag } private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) { boolean green = mPatternDisplayMode != DisplayMode.Wrong; final int endRow = end.row; final int startRow = start.row; final int endColumn = end.column; final int startColumn = start.column; // offsets for centering the bitmap in the cell final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2; final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2; // compute transform to place arrow bitmaps at correct angle inside circle. // This assumes that the arrow image is drawn at 12:00 with it's top edge // coincident with the circle bitmap's top edge. Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp; Matrix matrix = new Matrix(); final int cellWidth = mBitmapCircleDefault.getWidth(); final int cellHeight = mBitmapCircleDefault.getHeight(); // the up arrow bitmap is at 12:00, so find the rotation from x axis and add 90 degrees. final float theta = (float) Math.atan2( (double) (endRow - startRow), (double) (endColumn - startColumn)); final float angle = (float) Math.toDegrees(theta) + 90.0f; // compose matrix matrix.setTranslate(leftX + offsetX, topY + offsetY); // transform to cell position matrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate about cell center matrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate to 12:00 pos canvas.drawBitmap(arrow, matrix, mPaint); } /** * @param partOfPattern Whether this circle is part of the pattern. */ private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) { Bitmap outerCircle; Bitmap innerCircle; if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) { // unselected circle outerCircle = mBitmapCircleDefault; innerCircle = mBitmapBtnDefault; } else if (mPatternInProgress) { // user is in middle of drawing a pattern outerCircle = mBitmapCircleGreen; innerCircle = mBitmapBtnTouched; } else if (mPatternDisplayMode == DisplayMode.Wrong) { // the pattern is wrong outerCircle = mBitmapCircleRed; innerCircle = mBitmapBtnDefault; } else if (mPatternDisplayMode == DisplayMode.Correct || mPatternDisplayMode == DisplayMode.Animate) { // the pattern is correct outerCircle = mBitmapCircleGreen; innerCircle = mBitmapBtnDefault; } else { throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); } final int width = mBitmapWidth; final int height = mBitmapHeight; final float squareWidth = mSquareWidth; final float squareHeight = mSquareHeight; int offsetX = (int) ((squareWidth - width) / 2f); int offsetY = (int) ((squareHeight - height) / 2f); canvas.drawBitmap(outerCircle, leftX + offsetX, topY + offsetY, mPaint); canvas.drawBitmap(innerCircle, leftX + offsetX, topY + offsetY, mPaint); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, LockPatternUtils.patternToString(mPattern), mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode, mTactileFeedbackEnabled); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setPattern( DisplayMode.Correct, LockPatternUtils.stringToPattern(ss.getSerializedPattern())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInStealthMode = ss.isInStealthMode(); mTactileFeedbackEnabled = ss.isTactileFeedbackEnabled(); } /** * How to display the current pattern. */ public enum DisplayMode { /** * The pattern drawn is correct (i.e draw it in a friendly color) */ Correct, /** * Animate the pattern (for demo, and help). */ Animate, /** * The pattern is wrong (i.e draw a foreboding color) */ Wrong } /** * The call back interface for detecting patterns entered by the user. */ public interface OnPatternListener { /** * A new pattern has begun. */ void onPatternStart(); /** * The pattern was cleared. */ void onPatternCleared(); /** * A pattern was detected from the user. * * @param pattern The pattern. */ void onPatternDetected(List pattern); } /** * Represents a cell in the 3 X 3 matrix of the unlock pattern view. */ public static class Cell { // keep # objects limited to 9 static Cell[][] sCells = new Cell[3][3]; static { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { sCells[i][j] = new Cell(i, j); } } } int row; int column; /** * @param row The row of the cell. * @param column The column of the cell. */ private Cell(int row, int column) { checkRange(row, column); this.row = row; this.column = column; } /** * @param row The row of the cell. * @param column The column of the cell. */ public static synchronized Cell of(int row, int column) { checkRange(row, column); return sCells[row][column]; } private static void checkRange(int row, int column) { if (row < 0 || row > 2) { throw new IllegalArgumentException("row must be in range 0-2"); } if (column < 0 || column > 2) { throw new IllegalArgumentException("column must be in range 0-2"); } } public int getRow() { return row; } public int getColumn() { return column; } public String toString() { return "(row=" + row + ",clmn=" + column + ")"; } } /** * The parecelable for saving and restoring a lock pattern view. */ private static class SavedState extends BaseSavedState { public static final Parcelable.Creator CREATOR = new Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final boolean mInStealthMode; private final boolean mTactileFeedbackEnabled; /** * Constructor called from {@link LockPatternView#onSaveInstanceState()} */ private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInStealthMode = inStealthMode; mTactileFeedbackEnabled = tactileFeedbackEnabled; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInStealthMode = (Boolean) in.readValue(null); mTactileFeedbackEnabled = (Boolean) in.readValue(null); } public String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { return mInStealthMode; } public boolean isTactileFeedbackEnabled() { return mTactileFeedbackEnabled; } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeValue(mInStealthMode); dest.writeValue(mTactileFeedbackEnabled); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/provider/BankTransactionsProvider.java ================================================ /* * Copyright (C) 2010 Magnusart * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.provider; import com.liato.bankdroid.db.DatabaseHelper; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.preference.PreferenceManager; import java.util.HashMap; import java.util.Map; import timber.log.Timber; /** *

* This is the implementation of the BankTransactionsProvider. It provides * access to the transaction data for specific banks. *

* * @author Magnus Andersson * @see IBankTransactionsProvider * @since 8 jan 2011 */ public class BankTransactionsProvider extends ContentProvider implements IBankTransactionsProvider { private static final String CONTENT_PROVIDER_ENABLED = "content_provider_enabled"; private static final String CONTENT_PROVIDER_API_KEY = "content_provider_api_key"; private final static int TRANSACTIONS = 0; private final static int BANK_ACCOUNTS = 1; private static final String WILD_CARD = "*"; private static final String BANK_TABLE = "banks"; private static final String ACCOUNT_TABLE = "accounts"; private static final String BANK_ACCOUNT_TABLES = BANK_TABLE + " LEFT JOIN " + ACCOUNT_TABLE + " ON banks." + BANK_ID + " = accounts.bankid"; private static final String TRANSACTIONS_TABLE = "transactions"; private final static UriMatcher URI_MATCHER; private final static Map BANK_ACCOUNT_PROJECTION_MAP; private final static Map TRANS_PROJECTION_MAP; static { URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); URI_MATCHER.addURI(AUTHORITY, TRANSACTIONS_CAT + "/" + WILD_CARD, TRANSACTIONS); URI_MATCHER.addURI(AUTHORITY, BANK_ACCOUNTS_CAT + "/" + WILD_CARD, BANK_ACCOUNTS); // Projections are "Poor mans views" of the data. BANK_ACCOUNT_PROJECTION_MAP = new HashMap(); // Must match bankAccountProjection in // IBankTransactionsProvider#bankAccountProjection BANK_ACCOUNT_PROJECTION_MAP.put(BANK_ID, BANK_ID); BANK_ACCOUNT_PROJECTION_MAP.put(BANK_NAME, BANK_NAME); BANK_ACCOUNT_PROJECTION_MAP.put(BANK_TYPE, BANK_TYPE); BANK_ACCOUNT_PROJECTION_MAP.put(BANK_LAST_UPDATED, BANK_LAST_UPDATED); BANK_ACCOUNT_PROJECTION_MAP.put(ACC_ID, ACC_ID); BANK_ACCOUNT_PROJECTION_MAP.put(ACC_NAME, ACC_NAME); // Table name has to be explicitly included here since Banks also have a column named balance. BANK_ACCOUNT_PROJECTION_MAP.put(ACC_BALANCE, ACCOUNT_TABLE + "." + ACC_BALANCE); BANK_ACCOUNT_PROJECTION_MAP.put(ACC_TYPE, ACC_TYPE); TRANS_PROJECTION_MAP = new HashMap(); // Must match transactionProjection in // IBankTransactionsProvider#transactionProjection TRANS_PROJECTION_MAP.put(TRANS_ID, TRANS_ID); TRANS_PROJECTION_MAP.put(TRANS_DATE, TRANS_DATE); TRANS_PROJECTION_MAP.put(TRANS_DESC, TRANS_DESC); TRANS_PROJECTION_MAP.put(TRANS_AMT, TRANS_AMT); TRANS_PROJECTION_MAP.put(TRANS_CUR, TRANS_CUR); TRANS_PROJECTION_MAP.put(TRANS_ACCNT, TRANS_ACCNT); } private DatabaseHelper dbHelper; public static String getApiKey(final Context ctx) { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); if (!prefs.getBoolean(CONTENT_PROVIDER_ENABLED, false)) { throw new IllegalStateException( "Access to Content Provider is not enabled."); } final String apiKey = prefs.getString(CONTENT_PROVIDER_API_KEY, ""); if (apiKey.equals("")) { throw new IllegalArgumentException("The API-Key must be set."); } return apiKey; } /** * {@inheritDoc} */ @Override public int delete(final Uri uri, final String selection, final String[] selectionArgs) { throw new UnsupportedOperationException( "This provider does not implement the delete method"); } /** * {@inheritDoc} */ @Override public String getType(final Uri uri) { Timber.d("Got URI: %s", uri.toString()); switch (URI_MATCHER.match(uri)) { case BANK_ACCOUNTS: return BANK_ACCOUNTS_MIME; case TRANSACTIONS: return TRANSACTIONS_MIME; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } } /** * {@inheritDoc} */ @Override public Uri insert(final Uri uri, final ContentValues values) { throw new UnsupportedOperationException( "This provider does not implement the insert method"); } /** * {@inheritDoc} */ @Override public boolean onCreate() { dbHelper = DatabaseHelper.getHelper(getContext()); return true; } /** * {@inheritDoc} */ @Override public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { if (!isApiKeyEnabled(getContext())) { return null; } final String apiKey = uri.getPathSegments().get(1); Timber.v("Trying to access database with %s", apiKey); if (!apiKey.startsWith(API_KEY, 0)) { return null; // throw new IllegalArgumentException(API_KEY + // " must be a part of the URI!"); } final String key = apiKey.replace(API_KEY, ""); if (!key.equals(getApiKey(getContext()))) { return null; // throw new // IllegalAccessError("The supplied API_KEY does not exist"); } final SQLiteDatabase db = dbHelper.getReadableDatabase(); SQLiteQueryBuilder qb; if (BANK_ACCOUNTS_MIME.equals(getType(uri))) { qb = new SQLiteQueryBuilder(); qb.setTables(BANK_ACCOUNT_TABLES); qb.setProjectionMap(BANK_ACCOUNT_PROJECTION_MAP); qb.setDistinct(true); } else if (TRANSACTIONS_MIME.equals(getType(uri))) { qb = new SQLiteQueryBuilder(); qb.setTables(TRANSACTIONS_TABLE); qb.setProjectionMap(TRANS_PROJECTION_MAP); } else { throw new IllegalArgumentException("Unsupported URI: " + uri); } final Cursor cur = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); cur.setNotificationUri(getContext().getContentResolver(), uri); return cur; } /** * {@inheritDoc} */ @Override public int update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { throw new UnsupportedOperationException( "This provider does not implement the update method"); } private boolean isApiKeyEnabled(final Context ctx) { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); return prefs.getBoolean(CONTENT_PROVIDER_ENABLED, false); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/utils/EmulatorUtils.java ================================================ package com.liato.bankdroid.utils; import android.os.Build; import java.util.Arrays; import java.util.HashSet; import java.util.Properties; import java.util.Set; class EmulatorUtils { static final boolean RUNNING_ON_EMULATOR = isRunningOnEmulator(); static final boolean RUNNING_ON_ANDROID = isRunningOnAndroid(); private EmulatorUtils() { } private static boolean isRunningOnEmulator() { // Inspired by // http://stackoverflow.com/questions/2799097/how-can-i-detect-when-an-android-application-is-running-in> if (Build.PRODUCT == null) { return false; } Set parts = new HashSet<>(Arrays.asList(Build.PRODUCT.split("_"))); if (parts.size() == 0) { return false; } parts.remove("sdk"); parts.remove("google"); parts.remove("x86"); parts.remove("64"); parts.remove("phone"); // If the build identifier contains only the above keywords in some order, then we're // in an emulator return parts.isEmpty(); } private static boolean isRunningOnAndroid() { // Inspired by: // https://developer.android.com/reference/java/lang/System.html#getProperties() // Developed using trial and error... final Properties properties = System.getProperties(); final String httpAgent = (String) properties.get("http.agent"); return httpAgent != null && httpAgent.contains("Android"); } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java ================================================ package com.liato.bankdroid.utils; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.CustomEvent; import com.liato.bankdroid.BuildConfig; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import android.content.Context; import android.text.TextUtils; import android.util.Log; import io.fabric.sdk.android.Fabric; import timber.log.Timber; public class LoggingUtils { private static final boolean IS_CRASHLYTICS_ENABLED = isCrashlyticsEnabled(); private static final String DEFAULT_TAG = "Bankdroid"; private static Class initializedLoggingClass = null; private LoggingUtils() { } public static void createLogger(Context context) { Timber.Tree tree = IS_CRASHLYTICS_ENABLED ? new CrashlyticsTree(context) : new LocalTree(); if (initializedLoggingClass != Timber.class) { initializedLoggingClass = Timber.class; Timber.plant(tree); Timber.v("Logging tree planted: %s", tree.getClass()); } } private static boolean isCrashlyticsEnabled() { return EmulatorUtils.RUNNING_ON_ANDROID && !EmulatorUtils.RUNNING_ON_EMULATOR; } public static void logCustom(CustomEvent event) { if (!isCrashlyticsEnabled()) { return; } event.putCustomAttribute("App Version", BuildConfig.VERSION_NAME); Answers.getInstance().logCustom(event); } public static void logDisabledBank(Bank bank) { if (!isCrashlyticsEnabled()) { return; } logCustom(new CustomEvent("Disabled Bank"). putCustomAttribute("Name", bank.getName())); } public static void logBankUpdate(Bank bank, boolean withTransactions) { if (!isCrashlyticsEnabled()) { return; } logCustom(new CustomEvent("Bank Updated"). putCustomAttribute("Name", bank.getName()). putCustomAttribute("With Transactions", Boolean.toString(withTransactions))); boolean hasTransactions = false; for (Account account : bank.getAccounts()) { if (account.getTransactions() != null && !account.getTransactions().isEmpty()) { hasTransactions = true; } } if (withTransactions && !hasTransactions) { logCustom(new CustomEvent("Bank Without Transactions"). putCustomAttribute("Name", bank.getName())); } } private static class CrashlyticsTree extends Timber.Tree { CrashlyticsTree(Context context) { Fabric.with(context, new Crashlytics()); } @Override protected void log(int priority, String tag, String message, Throwable t) { if (BuildConfig.DEBUG) { tag = "DEBUG"; } else if (TextUtils.isEmpty(tag)) { tag = DEFAULT_TAG; } // This call logs to *both* Crashlytics and LogCat, and will log the Exception backtrace // to LogCat on exceptions. Crashlytics.log(priority, tag, message); if (t != null) { Crashlytics.logException(t); } } } private static class LocalTree extends Timber.Tree { @Override protected void log(int priority, String tag, String message, Throwable t) { if (BuildConfig.DEBUG) { tag = "DEBUG"; } else if (TextUtils.isEmpty(tag)) { tag = DEFAULT_TAG; } // Empirical evidence shows any exception stack trace is already part of the message, so // no need to print the exception explicitly here. Log.println(priority, tag, message); } } } ================================================ FILE: app/src/main/java/com/liato/bankdroid/utils/NetworkUtils.java ================================================ package com.liato.bankdroid.utils; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; public class NetworkUtils { private NetworkUtils() { } public static boolean isInternetAvailable() { return ping(new byte[]{8, 8, 8, 8}, 500); } private static boolean ping(byte[] ipAddress, int timeout) { DatagramSocket datagramSocket = null; try { datagramSocket = new DatagramSocket(); datagramSocket.setSoTimeout(timeout); datagramSocket.connect(InetAddress.getByAddress(ipAddress), 7); if (datagramSocket.isConnected()) { return true; } } catch (SocketException | UnknownHostException e) { return false; } finally { if (datagramSocket != null) { datagramSocket.close(); } } return false; } } ================================================ FILE: app/src/main/java/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java ================================================ /* * Copyright (C) 2010 Daniel Nilsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.margaritov.preference.colorpicker; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; /** * This drawable that draws a simple white and gray chessboard pattern. * It's pattern you will often see as a background behind a * partly transparent image in many applications. * * @author Daniel Nilsson */ public class AlphaPatternDrawable extends Drawable { private int mRectangleSize = 10; private Paint mPaint = new Paint(); private Paint mPaintWhite = new Paint(); private Paint mPaintGray = new Paint(); private int numRectanglesHorizontal; private int numRectanglesVertical; /** * Bitmap in which the pattern will be cahched. */ private Bitmap mBitmap; public AlphaPatternDrawable(int rectangleSize) { mRectangleSize = rectangleSize; mPaintWhite.setColor(0xffffffff); mPaintGray.setColor(0xffcbcbcb); } @Override public void draw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); } @Override public int getOpacity() { return 0; } @Override public void setAlpha(int alpha) { throw new UnsupportedOperationException("Alpha is not supported by this drawwable."); } @Override public void setColorFilter(ColorFilter cf) { throw new UnsupportedOperationException("ColorFilter is not supported by this drawwable."); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); int height = bounds.height(); int width = bounds.width(); numRectanglesHorizontal = width / mRectangleSize; numRectanglesVertical = height / mRectangleSize; generatePatternBitmap(); } /** * This will generate a bitmap with the pattern * as big as the rectangle we were allow to draw on. * We do this to chache the bitmap so we don't need to * recreate it each time draw() is called since it * takes a few milliseconds. */ private void generatePatternBitmap() { if (getBounds().width() <= 0 || getBounds().height() <= 0) { return; } mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888); Canvas canvas = new Canvas(mBitmap); Rect r = new Rect(); boolean verticalStartWhite = true; for (int i = 0; i <= numRectanglesVertical; i++) { boolean isWhite = verticalStartWhite; for (int j = 0; j <= numRectanglesHorizontal; j++) { r.top = i * mRectangleSize; r.left = j * mRectangleSize; r.bottom = r.top + mRectangleSize; r.right = r.left + mRectangleSize; canvas.drawRect(r, isWhite ? mPaintWhite : mPaintGray); isWhite = !isWhite; } verticalStartWhite = !verticalStartWhite; } } } ================================================ FILE: app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java ================================================ /* * Copyright (C) 2010 Daniel Nilsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.margaritov.preference.colorpicker; import com.liato.bankdroid.R; import android.app.Dialog; import android.content.Context; import android.graphics.PixelFormat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; public class ColorPickerDialog extends Dialog implements ColorPickerView.OnColorChangedListener, View.OnClickListener { private ColorPickerView mColorPicker; private ColorPickerPanelView mNewColor; private OnColorChangedListener mListener; private final ViewGroup mParent; public ColorPickerDialog(ViewGroup parent, Context context, int initialColor) { super(context); this.mParent = parent; init(initialColor); } private void init(int color) { // To fight color branding. getWindow().setFormat(PixelFormat.RGBA_8888); setUp(color); } private void setUp(int color) { LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); View layout = inflater.inflate(R.layout.dialog_color_picker, mParent, false); setContentView(layout); setTitle(R.string.dialog_color_picker); mColorPicker = (ColorPickerView) layout.findViewById(R.id.color_picker_view); ColorPickerPanelView oldColor = (ColorPickerPanelView) layout.findViewById(R.id.old_color_panel); mNewColor = (ColorPickerPanelView) layout.findViewById(R.id.new_color_panel); ((LinearLayout) oldColor.getParent()).setPadding( Math.round(mColorPicker.getDrawingOffset()), 0, Math.round(mColorPicker.getDrawingOffset()), 0 ); oldColor.setOnClickListener(this); mNewColor.setOnClickListener(this); mColorPicker.setOnColorChangedListener(this); oldColor.setColor(color); mColorPicker.setColor(color, true); } @Override public void onColorChanged(int color) { mNewColor.setColor(color); /* if (mListener != null) { mListener.onColorChanged(color); } */ } public void setAlphaSliderVisible(boolean visible) { mColorPicker.setAlphaSliderVisible(visible); } /** * Set a OnColorChangedListener to get notified when the color * selected by the user has changed. */ public void setOnColorChangedListener(OnColorChangedListener listener) { mListener = listener; } public int getColor() { return mColorPicker.getColor(); } @Override public void onClick(View v) { if (v.getId() == R.id.new_color_panel) { if (mListener != null) { mListener.onColorChanged(mNewColor.getColor()); } } dismiss(); } public interface OnColorChangedListener { void onColorChanged(int color); } } ================================================ FILE: app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPanelView.java ================================================ /* * Copyright (C) 2010 Daniel Nilsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.margaritov.preference.colorpicker; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; /** * This class draws a panel which which will be filled with a color which can be set. * It can be used to show the currently selected color which you will get from * the {@link ColorPickerView}. * * @author Daniel Nilsson */ public class ColorPickerPanelView extends View { /** * The width in pixels of the border * surrounding the color panel. */ private final static float BORDER_WIDTH_PX = 1; private float mDensity = 1f; private int mBorderColor = 0xff6E6E6E; private int mColor = 0xff000000; private Paint mBorderPaint; private Paint mColorPaint; private RectF mDrawingRect; private RectF mColorRect; private AlphaPatternDrawable mAlphaPattern; public ColorPickerPanelView(Context context) { this(context, null); } public ColorPickerPanelView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ColorPickerPanelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { mBorderPaint = new Paint(); mColorPaint = new Paint(); mDensity = getContext().getResources().getDisplayMetrics().density; } @Override protected void onDraw(Canvas canvas) { final RectF rect = mColorRect; if (BORDER_WIDTH_PX > 0) { mBorderPaint.setColor(mBorderColor); canvas.drawRect(mDrawingRect, mBorderPaint); } if (mAlphaPattern != null) { mAlphaPattern.draw(canvas); } mColorPaint.setColor(mColor); canvas.drawRect(rect, mColorPaint); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mDrawingRect = new RectF(); mDrawingRect.left = getPaddingLeft(); mDrawingRect.right = w - getPaddingRight(); mDrawingRect.top = getPaddingTop(); mDrawingRect.bottom = h - getPaddingBottom(); setUpColorRect(); } private void setUpColorRect() { final RectF dRect = mDrawingRect; float left = dRect.left + BORDER_WIDTH_PX; float top = dRect.top + BORDER_WIDTH_PX; float bottom = dRect.bottom - BORDER_WIDTH_PX; float right = dRect.right - BORDER_WIDTH_PX; mColorRect = new RectF(left, top, right, bottom); mAlphaPattern = new AlphaPatternDrawable((int) (5 * mDensity)); mAlphaPattern.setBounds( Math.round(mColorRect.left), Math.round(mColorRect.top), Math.round(mColorRect.right), Math.round(mColorRect.bottom) ); } /** * Get the color currently show by this view. */ public int getColor() { return mColor; } /** * Set the color that should be shown by this view. */ public void setColor(int color) { mColor = color; invalidate(); } /** * Get the color of the border surrounding the panel. */ public int getBorderColor() { return mBorderColor; } /** * Set the color of the border surrounding the panel. */ public void setBorderColor(int color) { mBorderColor = color; invalidate(); } } ================================================ FILE: app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java ================================================ /* * Copyright (C) 2011 Sergey Margaritov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.margaritov.preference.colorpicker; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Color; import android.preference.Preference; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import timber.log.Timber; /** * A preference type that allows a user to choose a time * * @author Sergey Margaritov */ public class ColorPickerPreference extends Preference implements Preference.OnPreferenceClickListener, ColorPickerDialog.OnColorChangedListener { private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; private ViewGroup parent; View mView; int mDefaultValue = Color.BLACK; private int mValue = Color.BLACK; private float mDensity = 0; private boolean mAlphaSliderEnabled = false; public ColorPickerPreference(Context context) { super(context); init(context, null); } public ColorPickerPreference(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ColorPickerPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } /** * For custom purposes. Not used by ColorPickerPreferrence * * @author Unknown */ public static String convertToARGB(int color) { String alpha = Integer.toHexString(Color.alpha(color)); String red = Integer.toHexString(Color.red(color)); String green = Integer.toHexString(Color.green(color)); String blue = Integer.toHexString(Color.blue(color)); if (alpha.length() == 1) { alpha = "0" + alpha; } if (red.length() == 1) { red = "0" + red; } if (green.length() == 1) { green = "0" + green; } if (blue.length() == 1) { blue = "0" + blue; } return "#" + alpha + red + green + blue; } /** * For custom purposes. Not used by ColorPickerPreferrence * * @author Unknown */ public static int convertToColorInt(String argb) throws NumberFormatException { if (argb.startsWith("#")) { argb = argb.replace("#", ""); } int alpha = -1, red = -1, green = -1, blue = -1; if (argb.length() == 8) { alpha = Integer.parseInt(argb.substring(0, 2), 16); red = Integer.parseInt(argb.substring(2, 4), 16); green = Integer.parseInt(argb.substring(4, 6), 16); blue = Integer.parseInt(argb.substring(6, 8), 16); } else if (argb.length() == 6) { alpha = 255; red = Integer.parseInt(argb.substring(0, 2), 16); green = Integer.parseInt(argb.substring(2, 4), 16); blue = Integer.parseInt(argb.substring(4, 6), 16); } return Color.argb(alpha, red, green, blue); } @Override protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { onColorChanged(restoreValue ? getValue() : (Integer) defaultValue); } private void init(Context context, AttributeSet attrs) { mDensity = getContext().getResources().getDisplayMetrics().density; setOnPreferenceClickListener(this); if (attrs != null) { String defaultValue = attrs.getAttributeValue(ANDROID_NS, "defaultValue"); if (defaultValue.startsWith("#")) { try { mDefaultValue = convertToColorInt(defaultValue); } catch (NumberFormatException e) { Timber.w("Wrong color: %s", defaultValue); mDefaultValue = convertToColorInt("#FF000000"); } } else { int resourceId = attrs.getAttributeResourceValue(ANDROID_NS, "defaultValue", 0); if (resourceId != 0) { mDefaultValue = context.getResources().getInteger(resourceId); } } mAlphaSliderEnabled = attrs.getAttributeBooleanValue(null, "alphaSlider", false); } mValue = mDefaultValue; } @Override protected View onCreateView(ViewGroup parent) { this.parent = parent; return super.onCreateView(parent); } @Override protected void onBindView(View view) { super.onBindView(view); mView = view; setPreviewColor(); } private void setPreviewColor() { if (mView == null) { return; } ImageView iView = new ImageView(getContext()); LinearLayout widgetFrameView = ((LinearLayout) mView .findViewById(android.R.id.widget_frame)); if (widgetFrameView == null) { return; } widgetFrameView.setPadding( widgetFrameView.getPaddingLeft(), widgetFrameView.getPaddingTop(), (int) (mDensity * 8), widgetFrameView.getPaddingBottom() ); // remove already create preview image int count = widgetFrameView.getChildCount(); if (count > 0) { widgetFrameView.removeViews(0, count); } widgetFrameView.setVisibility(View.VISIBLE); widgetFrameView.addView(iView); iView.setBackgroundDrawable(new AlphaPatternDrawable((int) (5 * mDensity))); iView.setImageBitmap(getPreviewBitmap()); } private Bitmap getPreviewBitmap() { int d = (int) (mDensity * 31); //30dip int color = getValue(); Bitmap bm = Bitmap.createBitmap(d, d, Config.ARGB_8888); int w = bm.getWidth(); int h = bm.getHeight(); int c = color; for (int i = 0; i < w; i++) { for (int j = i; j < h; j++) { c = (i <= 1 || j <= 1 || i >= w - 2 || j >= h - 2) ? Color.GRAY : color; bm.setPixel(i, j, c); if (i != j) { bm.setPixel(j, i, c); } } } return bm; } public int getValue() { try { if (isPersistent()) { mValue = getPersistedInt(mDefaultValue); } } catch (ClassCastException e) { mValue = mDefaultValue; } return mValue; } @Override public void onColorChanged(int color) { if (isPersistent()) { persistInt(color); } mValue = color; setPreviewColor(); OnPreferenceChangeListener listener = getOnPreferenceChangeListener(); if (listener != null) { listener.onPreferenceChange(this, color); } } @Override public boolean onPreferenceClick(Preference preference) { ColorPickerDialog picker = new ColorPickerDialog(parent, getContext(), getValue()); picker.setOnColorChangedListener(this); if (mAlphaSliderEnabled) { picker.setAlphaSliderVisible(true); } picker.show(); return false; } /** * Toggle Alpha Slider visibility (by default it's disabled) */ public void setAlphaSliderEnabled(boolean enable) { mAlphaSliderEnabled = enable; } } ================================================ FILE: app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java ================================================ /* * Copyright (C) 2010 Daniel Nilsson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.margaritov.preference.colorpicker; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ComposeShader; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * Displays a color picker to the user and allow them * to select a color. A slider for the alpha channel is * also available. Enable it by setting * setAlphaSliderVisible(boolean) to true. * * @author Daniel Nilsson */ public class ColorPickerView extends View { private final static int PANEL_SAT_VAL = 0; /* * To remember which panel that has the "focus" when * processing hardware button data. */ private int mLastTouchedPanel = PANEL_SAT_VAL; private final static int PANEL_HUE = 1; private final static int PANEL_ALPHA = 2; /** * The width in pixels of the border * surrounding all color panels. */ private final static float BORDER_WIDTH_PX = 1; /** * The width in dp of the hue panel. */ private float huePanelWidth = 30f; /** * The height in dp of the alpha panel */ private float alphaPanelHeight = 20f; /** * The distance in dp between the different * color panels. */ private float panelSpacing = 10f; /** * The radius in dp of the color palette tracker circle. */ private float paletteCircleTrackerRadius = 5f; /** * The dp which the tracker of the hue or alpha panel * will extend outside of its bounds. */ private float rectangleTrackerOffset = 2f; private float mDensity = 1f; private OnColorChangedListener mListener; private Paint mSatValPaint; private Paint mSatValTrackerPaint; private Paint mHuePaint; private Paint mHueTrackerPaint; private Paint mAlphaPaint; private Paint mAlphaTextPaint; private Paint mBorderPaint; private Shader mValShader; private Shader mSatShader; private Shader mHueShader; private Shader mAlphaShader; private int mAlpha = 0xff; private float mHue = 360f; private float mSat = 0f; private float mVal = 0f; private String mAlphaSliderText = ""; private int mSliderTrackerColor = 0xff1c1c1c; private int mBorderColor = 0xff6E6E6E; private boolean mShowAlphaPanel = false; /** * Offset from the edge we must have or else * the finger tracker will get clipped when * it is drawn outside of the view. */ private float mDrawingOffset; /* * Distance form the edges of the view * of where we are allowed to draw. */ private RectF mDrawingRect; private RectF mSatValRect; private RectF mHueRect; private RectF mAlphaRect; private AlphaPatternDrawable mAlphaPattern; private Point mStartTouchPoint = null; public ColorPickerView(Context context) { this(context, null); } public ColorPickerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { if (Integer.parseInt(android.os.Build.VERSION.SDK) >= 11) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } mDensity = getContext().getResources().getDisplayMetrics().density; paletteCircleTrackerRadius *= mDensity; rectangleTrackerOffset *= mDensity; huePanelWidth *= mDensity; alphaPanelHeight *= mDensity; panelSpacing = panelSpacing * mDensity; mDrawingOffset = calculateRequiredOffset(); initPaintTools(); //Needed for receiving trackball motion events. setFocusable(true); setFocusableInTouchMode(true); } private void initPaintTools() { mSatValPaint = new Paint(); mSatValTrackerPaint = new Paint(); mHuePaint = new Paint(); mHueTrackerPaint = new Paint(); mAlphaPaint = new Paint(); mAlphaTextPaint = new Paint(); mBorderPaint = new Paint(); mSatValTrackerPaint.setStyle(Style.STROKE); mSatValTrackerPaint.setStrokeWidth(2f * mDensity); mSatValTrackerPaint.setAntiAlias(true); mHueTrackerPaint.setColor(mSliderTrackerColor); mHueTrackerPaint.setStyle(Style.STROKE); mHueTrackerPaint.setStrokeWidth(2f * mDensity); mHueTrackerPaint.setAntiAlias(true); mAlphaTextPaint.setColor(0xff1c1c1c); mAlphaTextPaint.setTextSize(14f * mDensity); mAlphaTextPaint.setAntiAlias(true); mAlphaTextPaint.setTextAlign(Align.CENTER); mAlphaTextPaint.setFakeBoldText(true); } private float calculateRequiredOffset() { float offset = Math.max(paletteCircleTrackerRadius, rectangleTrackerOffset); offset = Math.max(offset, BORDER_WIDTH_PX * mDensity); return offset * 1.5f; } private int[] buildHueColorArray() { int[] hue = new int[361]; int count = 0; for (int i = hue.length - 1; i >= 0; i--, count++) { hue[count] = Color.HSVToColor(new float[]{i, 1f, 1f}); } return hue; } @Override protected void onDraw(Canvas canvas) { if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) { return; } drawSatValPanel(canvas); drawHuePanel(canvas); drawAlphaPanel(canvas); } private void drawSatValPanel(Canvas canvas) { final RectF rect = mSatValRect; if (BORDER_WIDTH_PX > 0) { mBorderPaint.setColor(mBorderColor); canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); } if (mValShader == null) { mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, 0xffffffff, 0xff000000, TileMode.CLAMP); } int rgb = Color.HSVToColor(new float[]{mHue, 1f, 1f}); mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, TileMode.CLAMP); ComposeShader mShader = new ComposeShader(mValShader, mSatShader, PorterDuff.Mode.MULTIPLY); mSatValPaint.setShader(mShader); canvas.drawRect(rect, mSatValPaint); Point p = satValToPoint(mSat, mVal); mSatValTrackerPaint.setColor(0xff000000); canvas.drawCircle(p.x, p.y, paletteCircleTrackerRadius - 1f * mDensity, mSatValTrackerPaint); mSatValTrackerPaint.setColor(0xffdddddd); canvas.drawCircle(p.x, p.y, paletteCircleTrackerRadius, mSatValTrackerPaint); } private void drawHuePanel(Canvas canvas) { final RectF rect = mHueRect; if (BORDER_WIDTH_PX > 0) { mBorderPaint.setColor(mBorderColor); canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); } if (mHueShader == null) { mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, buildHueColorArray(), null, TileMode.CLAMP); mHuePaint.setShader(mHueShader); } canvas.drawRect(rect, mHuePaint); float rectHeight = 4 * mDensity / 2; Point p = hueToPoint(mHue); RectF r = new RectF(); r.left = rect.left - rectangleTrackerOffset; r.right = rect.right + rectangleTrackerOffset; r.top = p.y - rectHeight; r.bottom = p.y + rectHeight; canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); } private void drawAlphaPanel(Canvas canvas) { if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) { return; } final RectF rect = mAlphaRect; if (BORDER_WIDTH_PX > 0) { mBorderPaint.setColor(mBorderColor); canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); } mAlphaPattern.draw(canvas); float[] hsv = new float[]{mHue, mSat, mVal}; int color = Color.HSVToColor(hsv); int acolor = Color.HSVToColor(0, hsv); mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor, TileMode.CLAMP); mAlphaPaint.setShader(mAlphaShader); canvas.drawRect(rect, mAlphaPaint); if (mAlphaSliderText != null && mAlphaSliderText != "") { canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity, mAlphaTextPaint); } float rectWidth = 4 * mDensity / 2; Point p = alphaToPoint(mAlpha); RectF r = new RectF(); r.left = p.x - rectWidth; r.right = p.x + rectWidth; r.top = rect.top - rectangleTrackerOffset; r.bottom = rect.bottom + rectangleTrackerOffset; canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); } private Point hueToPoint(float hue) { final RectF rect = mHueRect; final float height = rect.height(); Point p = new Point(); p.y = (int) (height - (hue * height / 360f) + rect.top); p.x = (int) rect.left; return p; } private Point satValToPoint(float sat, float val) { final RectF rect = mSatValRect; final float height = rect.height(); final float width = rect.width(); Point p = new Point(); p.x = (int) (sat * width + rect.left); p.y = (int) ((1f - val) * height + rect.top); return p; } private Point alphaToPoint(int alpha) { final RectF rect = mAlphaRect; final float width = rect.width(); Point p = new Point(); p.x = (int) (width - (alpha * width / 0xff) + rect.left); p.y = (int) rect.top; return p; } private float[] pointToSatVal(float x, float y) { final RectF rect = mSatValRect; float[] result = new float[2]; float width = rect.width(); float height = rect.height(); if (x < rect.left) { x = 0f; } else if (x > rect.right) { x = width; } else { x = x - rect.left; } if (y < rect.top) { y = 0f; } else if (y > rect.bottom) { y = height; } else { y = y - rect.top; } result[0] = 1.f / width * x; result[1] = 1.f - (1.f / height * y); return result; } private float pointToHue(float y) { final RectF rect = mHueRect; float height = rect.height(); if (y < rect.top) { y = 0f; } else if (y > rect.bottom) { y = height; } else { y = y - rect.top; } return 360f - (y * 360f / height); } private int pointToAlpha(int x) { final RectF rect = mAlphaRect; final int width = (int) rect.width(); if (x < rect.left) { x = 0; } else if (x > rect.right) { x = width; } else { x = x - (int) rect.left; } return 0xff - (x * 0xff / width); } @Override public boolean onTrackballEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); boolean update = false; if (event.getAction() == MotionEvent.ACTION_MOVE) { switch (mLastTouchedPanel) { case PANEL_SAT_VAL: float sat, val; sat = mSat + x / 50f; val = mVal - y / 50f; if (sat < 0f) { sat = 0f; } else if (sat > 1f) { sat = 1f; } if (val < 0f) { val = 0f; } else if (val > 1f) { val = 1f; } mSat = sat; mVal = val; update = true; break; case PANEL_HUE: float hue = mHue - y * 10f; if (hue < 0f) { hue = 0f; } else if (hue > 360f) { hue = 360f; } mHue = hue; update = true; break; case PANEL_ALPHA: if (!mShowAlphaPanel || mAlphaRect == null) { update = false; } else { int alpha = (int) (mAlpha - x * 10); if (alpha < 0) { alpha = 0; } else if (alpha > 0xff) { alpha = 0xff; } mAlpha = alpha; update = true; } break; } } if (update) { if (mListener != null) { mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); } invalidate(); return true; } return super.onTrackballEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { boolean update = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mStartTouchPoint = new Point((int) event.getX(), (int) event.getY()); update = moveTrackersIfNeeded(event); break; case MotionEvent.ACTION_MOVE: update = moveTrackersIfNeeded(event); break; case MotionEvent.ACTION_UP: mStartTouchPoint = null; update = moveTrackersIfNeeded(event); break; } if (update) { if (mListener != null) { mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); } invalidate(); return true; } return super.onTouchEvent(event); } private boolean moveTrackersIfNeeded(MotionEvent event) { if (mStartTouchPoint == null) { return false; } boolean update = false; int startX = mStartTouchPoint.x; int startY = mStartTouchPoint.y; if (mHueRect.contains(startX, startY)) { mLastTouchedPanel = PANEL_HUE; mHue = pointToHue(event.getY()); update = true; } else if (mSatValRect.contains(startX, startY)) { mLastTouchedPanel = PANEL_SAT_VAL; float[] result = pointToSatVal(event.getX(), event.getY()); mSat = result[0]; mVal = result[1]; update = true; } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) { mLastTouchedPanel = PANEL_ALPHA; mAlpha = pointToAlpha((int) event.getX()); update = true; } return update; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0; int height = 0; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthAllowed = MeasureSpec.getSize(widthMeasureSpec); int heightAllowed = MeasureSpec.getSize(heightMeasureSpec); widthAllowed = chooseWidth(widthMode, widthAllowed); heightAllowed = chooseHeight(heightMode, heightAllowed); if (!mShowAlphaPanel) { height = (int) (widthAllowed - panelSpacing - huePanelWidth); //If calculated height (based on the width) is more than the allowed height. if (height > heightAllowed || getTag().equals("landscape")) { height = heightAllowed; width = (int) (height + panelSpacing + huePanelWidth); } else { width = widthAllowed; } } else { width = (int) (heightAllowed - alphaPanelHeight + huePanelWidth); if (width > widthAllowed) { width = widthAllowed; height = (int) (widthAllowed - huePanelWidth + alphaPanelHeight); } else { height = heightAllowed; } } setMeasuredDimension(width, height); } private int chooseWidth(int mode, int size) { if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { return size; } else { // (mode == MeasureSpec.UNSPECIFIED) return getPrefferedWidth(); } } private int chooseHeight(int mode, int size) { if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { return size; } else { // (mode == MeasureSpec.UNSPECIFIED) return getPrefferedHeight(); } } private int getPrefferedWidth() { int width = getPrefferedHeight(); if (mShowAlphaPanel) { width -= (panelSpacing + alphaPanelHeight); } return (int) (width + huePanelWidth + panelSpacing); } private int getPrefferedHeight() { int height = (int) (200 * mDensity); if (mShowAlphaPanel) { height += panelSpacing + alphaPanelHeight; } return height; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mDrawingRect = new RectF(); mDrawingRect.left = mDrawingOffset + getPaddingLeft(); mDrawingRect.right = w - mDrawingOffset - getPaddingRight(); mDrawingRect.top = mDrawingOffset + getPaddingTop(); mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom(); setUpSatValRect(); setUpHueRect(); setUpAlphaRect(); } private void setUpSatValRect() { final RectF dRect = mDrawingRect; float panelSide = dRect.height() - BORDER_WIDTH_PX * 2; if (mShowAlphaPanel) { panelSide -= panelSpacing + alphaPanelHeight; } float left = dRect.left + BORDER_WIDTH_PX; float top = dRect.top + BORDER_WIDTH_PX; float bottom = top + panelSide; float right = left + panelSide; mSatValRect = new RectF(left, top, right, bottom); } private void setUpHueRect() { final RectF dRect = mDrawingRect; float left = dRect.right - huePanelWidth + BORDER_WIDTH_PX; float top = dRect.top + BORDER_WIDTH_PX; float bottom = dRect.bottom - BORDER_WIDTH_PX - (mShowAlphaPanel ? (panelSpacing + alphaPanelHeight) : 0); float right = dRect.right - BORDER_WIDTH_PX; mHueRect = new RectF(left, top, right, bottom); } private void setUpAlphaRect() { if (!mShowAlphaPanel) { return; } final RectF dRect = mDrawingRect; float left = dRect.left + BORDER_WIDTH_PX; float top = dRect.bottom - alphaPanelHeight + BORDER_WIDTH_PX; float bottom = dRect.bottom - BORDER_WIDTH_PX; float right = dRect.right - BORDER_WIDTH_PX; mAlphaRect = new RectF(left, top, right, bottom); mAlphaPattern = new AlphaPatternDrawable((int) (5 * mDensity)); mAlphaPattern.setBounds( Math.round(mAlphaRect.left), Math.round(mAlphaRect.top), Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom) ); } /** * Set a OnColorChangedListener to get notified when the color * selected by the user has changed. */ public void setOnColorChangedListener(OnColorChangedListener listener) { mListener = listener; } /** * Get the color of the border surrounding all panels. */ public int getBorderColor() { return mBorderColor; } /** * Set the color of the border surrounding all panels. */ public void setBorderColor(int color) { mBorderColor = color; invalidate(); } /** * Get the current color this view is showing. * * @return the current color. */ public int getColor() { return Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal}); } /** * Set the color the view should show. * * @param color The color that should be selected. */ public void setColor(int color) { setColor(color, false); } /** * Set the color this view should show. * * @param color The color that should be selected. * @param callback If you want to get a callback to * your OnColorChangedListener. */ public void setColor(int color, boolean callback) { int alpha = Color.alpha(color); int red = Color.red(color); int blue = Color.blue(color); int green = Color.green(color); float[] hsv = new float[3]; Color.RGBToHSV(red, green, blue, hsv); mAlpha = alpha; mHue = hsv[0]; mSat = hsv[1]; mVal = hsv[2]; if (callback && mListener != null) { mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); } invalidate(); } /** * Get the drawing offset of the color picker view. * The drawing offset is the distance from the side of * a panel to the side of the view minus the padding. * Useful if you want to have your own panel below showing * the currently selected color and want to align it perfectly. * * @return The offset in pixels. */ public float getDrawingOffset() { return mDrawingOffset; } /** * Set if the user is allowed to adjust the alpha panel. Default is false. * If it is set to false no alpha will be set. */ public void setAlphaSliderVisible(boolean visible) { if (mShowAlphaPanel != visible) { mShowAlphaPanel = visible; /* * Reset all shader to force a recreation. * Otherwise they will not look right after * the size of the view has changed. */ mValShader = null; mSatShader = null; mHueShader = null; mAlphaShader = null; requestLayout(); } } public int getSliderTrackerColor() { return mSliderTrackerColor; } public void setSliderTrackerColor(int color) { mSliderTrackerColor = color; mHueTrackerPaint.setColor(mSliderTrackerColor); invalidate(); } /** * Set the text that should be shown in the * alpha slider. Set to null to disable text. * * @param res string resource id. */ public void setAlphaSliderText(int res) { String text = getContext().getString(res); setAlphaSliderText(text); } /** * Get the current value of the text * that will be shown in the alpha * slider. */ public String getAlphaSliderText() { return mAlphaSliderText; } /** * Set the text that should be shown in the * alpha slider. Set to null to disable text. * * @param text Text that should be shown. */ public void setAlphaSliderText(String text) { mAlphaSliderText = text; invalidate(); } public interface OnColorChangedListener { void onColorChanged(int color); } } ================================================ FILE: app/src/main/java/net/sf/andhsli/hotspotlogin/SimpleCrypto.java ================================================ /* * Copyright (c) 2009 Ferenc Hechler - ferenc_hechler@users.sourceforge.net * http://www.androidsnippets.org/snippets/39/index.html */ package net.sf.andhsli.hotspotlogin; import com.liato.bankdroid.utils.StringUtils; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; /** * Usage: *
 * String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
 * ...
 * String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
 * 
* * @deprecated Broken * on Android Nougat, * considered * broken by Android Lint even before then. * * @author ferenc.hechler */ public class SimpleCrypto { public static String decrypt(String seed, String encrypted) throws Exception { byte[] rawKey = getRawKey(StringUtils.getBytes(seed)); byte[] enc = toByte(encrypted); byte[] result = decrypt(rawKey, enc); return StringUtils.toString(result); } private static byte[] getRawKey(byte[] seed) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); SecureRandom sr; if (android.os.Build.VERSION.SDK_INT >= 17) { sr = SecureRandom.getInstance("SHA1PRNG", "Crypto"); } else { sr = SecureRandom.getInstance("SHA1PRNG"); } sr.setSeed(seed); kgen.init(128, sr); // 192 and 256 bits may not be available SecretKey skey = kgen.generateKey(); byte[] raw = skey.getEncoded(); return raw; } private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception { SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, skeySpec); byte[] decrypted = cipher.doFinal(encrypted); return decrypted; } private static byte[] toByte(String hexString) { int len = hexString.length() / 2; byte[] result = new byte[len]; for (int i = 0; i < len; i++) { result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue(); } return result; } } ================================================ FILE: app/src/main/res/anim/grow_from_bottom.xml ================================================ ================================================ FILE: app/src/main/res/anim/grow_from_top.xml ================================================ ================================================ FILE: app/src/main/res/anim/grow_from_topleft_to_bottomright.xml ================================================ ================================================ FILE: app/src/main/res/anim/shrink_from_bottom.xml ================================================ ================================================ FILE: app/src/main/res/anim/shrink_from_bottomright_to_topleft.xml ================================================ ================================================ FILE: app/src/main/res/anim/shrink_from_top.xml ================================================ ================================================ FILE: app/src/main/res/anim/zoom_enter.xml ================================================ ================================================ FILE: app/src/main/res/anim/zoom_exit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/btn_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable/lock_anim.xml ================================================ ================================================ FILE: app/src/main/res/drawable/menu_button.xml ================================================ ================================================ FILE: app/src/main/res/drawable/popup_button.xml ================================================ ================================================ FILE: app/src/main/res/drawable/widget_progress.xml ================================================ ================================================ FILE: app/src/main/res/layout/about.xml ================================================ ================================================ FILE: app/src/main/res/layout/bank.xml ================================================ ================================================ FILE: app/src/main/res/layout/bank_spinner_dropdown_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/bank_spinner_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/choose_lock_pattern.xml ================================================ ================================================ FILE: app/src/main/res/layout/pair_applications_layout.xml ================================================ ================================================ FILE: app/src/main/res/layout/popup_account.xml ================================================ ================================================ FILE: app/src/main/res/layout/popup_bank.xml ================================================ ================================================ FILE: app/src/main/res/layout/toolbar.xml ================================================ ================================================ FILE: app/src/main/res/layout/transaction_date.xml ================================================ ================================================ FILE: app/src/main/res/layout/transaction_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/transactions.xml ================================================ ================================================ FILE: app/src/main/res/layout/webview.xml ================================================ ================================================ FILE: app/src/main/res/layout/widget.xml ================================================ ================================================ FILE: app/src/main/res/layout/widget_large.xml ================================================ ================================================ FILE: app/src/main/res/layout/widget_large_transparent.xml ================================================ ================================================ FILE: app/src/main/res/layout/widget_transparent.xml ================================================ ================================================ FILE: app/src/main/res/layout-land/choose_lock_pattern.xml ================================================