diff --git a/.gitignore b/.gitignore index c072012f3..d37593532 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,109 @@ +###Android### +# built application files +*.apk +*.ap_ + +# built NDK stuff +libs/armeabi +libs/armeabi-v7a +obj/ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files bin/ gen/ + +# capture hprofs +captures + +# Local configuration file (sdk path, etc) +local.properties + +# Eclipse project files +.classpath +.project + + +###Eclipse### + +*.pydevproject .project .metadata +bin/** tmp/** tmp/**/* *.tmp *.bak *.swp -*~ -lint.xml - +*~.nib +local.properties .classpath .settings/ .loadpath +# External tool builders +.externalToolBuilders/ + # Locally stored "Eclipse launch configurations" *.launch -# built application files -*.apk -*.ap_ +# CDT-specific +.cproject +# PDT-specific +.buildpath -# Java class files -*.class -local.properties -project.properties -notes +###Maven### + +target/ + + +###Gradle### + +.gradle/ +build/ + + +###IntelliJ### + +*.iml +*.ipr +*.iws +.idea/ + + +###Actionscript### + +# Build and Release Folders +bin/ +bin-debug/ +bin-release/ + +# Project property files +.actionScriptProperties +.flexProperties +.settings/ +.project + +###OSX### + +.DS_Store + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +###Signing### +signingconfig.gradle +signing.properties +*.keystore diff --git a/.tx/config b/.tx/config new file mode 100644 index 000000000..329a2dc57 --- /dev/null +++ b/.tx/config @@ -0,0 +1,9 @@ +[main] +host = https://www.transifex.com +lang_map = en_GB: en-rGB, af_ZA: af-rZA, am_ET: am-rET, ar_AA: ar-rAA, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: sy-rIQ, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, ast_ES: ast-rES, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, eo: eo-rXX, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_419: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, he_IL: he-rIL, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, ku_IQ: ckb-rIQ, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, my_MM : my-rMM, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, sd: sd-rPK, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr@latin: sr-rYU, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tet: tet-rTL, tg_TJ: tg-rTJ, tl_PH: tl-rPH, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz@Latn: uz-rUZ, uz@Cyrl: uzb-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA + +[tomahawk-android.stringsxml-72] +file_filter = app/src/main/res/values-/strings.xml +source_file = app/src/main/res/values/strings.xml +source_lang = en +type = ANDROID diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 41754a566..000000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index a6b2adc57..430baf714 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,69 @@ -tomahawk-android -================ - -Tomahawk's Android Music Player - -setup -================ - - - Open Eclipse and go to "File"->"Import" - - Under Android/ select "Existing Android Code into Workspace." - - Browse to your tomahawk-android checkout. - - Two projects will appear in the import dialog. Import them both. - One is the app and one is the unit tests. - - Right click on "tomahawk-android-test" and select "Properties". Now - select "Java Build Path" and the tab "Projects". Click on "Add" and - choose "tomahawk-android". Finish by clicking "OK". - - tomahawk-android requires the third-party support library - "ActionBarSherlock". Download and extract the library: - - https://github.com/JakeWharton/ActionBarSherlock/zipball/4.1.0 - - Now add it as an "Android Project" to your workspace: - "File"-> "Import" -> "Android" -> "Existing Android Code into Workspace" - - Go into the folder you've extracted your downloaded zip-file to and - choose the "library" folder as your "Root Directory". - - Check "copy projects into workspace" and click "Finish". - - Since the 4.1.0 release of ActionBarSherlock does include an outdated copy - of the android support package v4, you'll need to update that manually by - doing the following: - - Copy "/android-sdk-linux/extras/android/support/v4/android-support-v4.jar" - into the just created ActionBarSherlock project's "lib" folder. - Confirm if asked to overwrite the existing "android-support-v4.jar". - - Now add the just created library project to tomahawk-android by - rightclicking your "tomahawk-android" project and selecting "Properties" - - Select "Android" and add the library by clicking "Add...". - - To finish the process, choose your ActionBarSherlock library project and - click "OK". - - Notes: - - There is a known issue when importing. The primary app name - ends up being "org.tomahawk.tomahawk_android.TomahawkMainActivity". - Right click on the project and go to "Refactor"->"Rename". Rename - the project to "tomahawk-android" and this should fix any errors. - - If you have troubles building ActionBarSherlock, confirm that you have android-14 installed - in the sdk. This version is needed to build ActionBarSherlock, but you should use latest version - to build Tomahawk-Android. - - If you have other build problems, confirm that your Java Compiler is set to v1.6. - ( in eclipse, go to tomahawk-android ( right click ) -> Properties -> Java Compiler -> - Compiler compliance level -> 1.6 ) - - Make sure that you dont tick the Is Library box in Properties->Android || Library. - - It is also good to add the sdk to your path. - -required reading -================ - - http://developer.android.com/reference/android/os/Handler.html - - http://developer.android.com/guide/practices/screens_support.html +## tomahawk-android +*Music is everywhere, now you don’t have to be!* + +Tomahawk, the critically acclaimed multi-source music player, is now available on Android. Given the name of an artist, album or song Tomahawk will find the best available source and play it - whether that be from Spotify, Deezer, GMusic, Soundcloud, Tidal, Official.fm, Jamendo, Beets, Ampache, Subsonic or your phone’s local storage. +Tomahawk for Android also syncs your history, your loved tracks, artists, albums and your playlists to/from the desktop version of Tomahawk via our new music community, Hatchet. On Hatchet you can hear your friends' favorite tracks and see what they're currently listening to. + +![Tomahawk Screenshot1](/screenshots/screenshot1.png) | ![Tomahawk Screenshot2](/screenshots/screenshot2.png) | ![Tomahawk Screenshot3](/screenshots/screenshot3.png) +------ | ----- | ----- + +## Beta and Nightly + +Get the Beta version on Google Play: +https://play.google.com/store/apps/details?id=org.tomahawk.tomahawk_android + +Nightly builds are available here: +http://download.tomahawk-player.org/nightly/android/?C=M;O=D + +## Development Setup + +First of all you have to properly setup your Android SDK/NDK: + +- Download and install the Android SDK http://developer.android.com/sdk/index.html + - Make sure you have updated and installed the following in your Android SDK Manager: + - "/Tools" + - the latest Android SDK Platform folder (e.g. "/Android 6.0 (API 23)") + - "/Extras/Android Support Repository" and "/Extras/Android Support Library" + - "/Extras/Google Play Services" and "/Extras/Google Repository" + +Build it on the commandline with gradle: + +- Simply run "./gradlew assembleDebug" for the debug build or "./gradlew assembleRelease" for + the release build in your tomahawk-android checkout directory. The built apk will be put into + "tomahawk-android/build/outputs/apk" + +Setup using Android Studio and gradle (highly recommended): + +- Open Android Studio and go to "File"->"Import Project" +- Browse to your tomahawk-android checkout and click "OK". +- Make sure that the radio-button "Use default gradle wrapper (recommended)" is selected. +- Click "next" and that's it :) tomahawk-android should compile right away + +Setup using other IDEs without gradle: + +- Import tomahawk-android into the IDE of your choice +- tomahawk-android depends on several 3rd party libraries. You can look up a list of all dependencies in ./app/build.gradle under dependencies{...} +- Make sure you setup the support libraries correctly (http://developer.android.com/tools/support-library/setup.html) +- Add all dependencies to your tomahawk-android project +- tomahawk-android should now compile successfully. + +If you have any further problems, feel free to join the #tomahawk.mobile irc channel on irc.freenode.org + +## Ready to contribute? + +Drop us an e-mail at welisten@tomahawk-player.org or join our IRC Channel #tomahawk.mobile on irc.freenode.org + +## Code Style Guidelines for Contributors + +In order to keep everything clean and cozy, please use the official Android code style format preset: +- https://github.com/android/platform_development/tree/master/ide + (use the IntelliJ preset, if you're using Android Studio) + +For a larger overview you should read the official Android "Code Style Guidelines for Contributors": +- http://source.android.com/source/code-style.html + +## Plugin Apps Source Code + +[Spotify Plugin App](https://github.com/tomahawk-player/tomahawk-android-spotify) +[Deezer Plugin App](https://github.com/tomahawk-player/tomahawk-android-deezer) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..1a1069d73 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,154 @@ +apply plugin: "com.android.application" + +repositories { + flatDir { + dirs 'libs' + } +} + +android { + compileSdkVersion 23 + buildToolsVersion '23.0.3' + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 22 + renderscriptTargetApi 20 + renderscriptSupportModeEnabled true + def name = readVersionName() + def parts = name.split("[\\._-]") + def code = parts[0] + parts[1] + code = String.format("%-5s", code.substring(0, Math.min(5, code.size()))).replace(' ', '0') + code = Integer.parseInt(code) + versionName name + versionCode code + println("Using version name: $name") + println("Using version code: $code") + } + + lintOptions { + abortOnError false + } + + dexOptions { + jumboMode true + javaMaxHeapSize "2g" + } + + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/ASL2.0' + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def outputFile = output.outputFile + if (outputFile != null && outputFile.name.endsWith('.apk')) { + def fileName = outputFile.name. + replace(".apk", "-" + defaultConfig.versionName + ".apk") + output.outputFile = new File(outputFile.parent, fileName) + } + } + } + + signingConfigs { + release + } + + buildTypes { + release { + zipAlignEnabled true + minifyEnabled true + proguardFiles "../proguard-android.txt" + } + debug { + versionNameSuffix "_debug" + zipAlignEnabled true + } + } + + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64' + universalApk true + } + } +} + +// map for the version code +ext.versionCodes = ['armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 4, 'x86': 6, 'x86_64': 7] + +android.applicationVariants.all { variant -> + // assign different version code for each output + variant.outputs.each { output -> + output.versionCodeOverride = + project.ext.versionCodes. + get(output.getFilter(com.android.build.OutputFile.ABI), 9) * 100000 + + android.defaultConfig.versionCode + println("Using version name: $output.versionCodeOverride") + } + // assign different version name for each output + variant.outputs.each { output -> + def suffix = "_universal" + if (output.getFilter(com.android.build.OutputFile.ABI) != null) { + suffix = "_" + output.getFilter(com.android.build.OutputFile.ABI) + } + output.versionNameOverride = android.defaultConfig.versionName + suffix + } +} + +def Properties props = new Properties() +def propFile = file('signing.properties') +if (propFile.canRead()) { + props.load(new FileInputStream(propFile)) + + if (props != null && props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') && + props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) { + android.signingConfigs.release.storeFile = file(props['STORE_FILE']) + android.signingConfigs.release.storePassword = props['STORE_PASSWORD'] + android.signingConfigs.release.keyAlias = props['KEY_ALIAS'] + android.signingConfigs.release.keyPassword = props['KEY_PASSWORD'] + android.buildTypes.release.signingConfig = android.signingConfigs.release + android.buildTypes.debug.signingConfig = android.signingConfigs.release + } +} + +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile(name: 'circularprogressview-debug', ext: 'aar') + compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:support-v4:24.1.1' + compile 'se.emilsjolander:stickylistheaders:2.7.0' + compile('ch.acra:acra:4.7.0') { + transitive = false + } + compile 'com.google.code.gson:gson:2.5' + compile 'com.google.android.gms:play-services-base:8.4.0' + compile('com.stanfy:gson-xml-java:0.1.7') { + exclude group: 'xmlpull', module: 'xmlpull' + } + compile 'com.squareup.picasso:picasso:2.5.2' + compile 'com.squareup.okhttp:okhttp:2.7.2' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.7.2' + compile 'com.squareup.okhttp:logging-interceptor:2.7.2' + compile 'com.github.castorflex.smoothprogressbar:library:1.1.0' + compile 'de.mrmaffen:vlc-android-sdk:1.9.8' + compile 'org.apache.lucene:lucene-core:4.7.2' + compile 'org.apache.lucene:lucene-analyzers-common:4.7.2' + compile 'org.apache.lucene:lucene-queryparser:4.7.2' + compile 'commons-io:commons-io:2.4' + compile 'net.sourceforge.findbugs:jsr305:1.3.7' + compile 'com.squareup.retrofit:retrofit:1.9.0' + compile 'com.sothree.slidinguppanel:library:3.2.1' + compile 'com.uservoice:uservoice-android-sdk:1.2.4' + compile 'de.greenrobot:eventbus:2.4.1' + compile 'com.daimajia.swipelayout:library:1.2.0@aar' + compile 'org.jdeferred:jdeferred-android-aar:1.2.4' + compile 'org.slf4j:slf4j-android:1.7.13' +} diff --git a/app/libs/RemoteMetadataProvider.jar b/app/libs/RemoteMetadataProvider.jar new file mode 100644 index 000000000..b3ab62e01 Binary files /dev/null and b/app/libs/RemoteMetadataProvider.jar differ diff --git a/app/libs/circularprogressview-debug.aar b/app/libs/circularprogressview-debug.aar new file mode 100644 index 000000000..374040704 Binary files /dev/null and b/app/libs/circularprogressview-debug.aar differ diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 000000000..437c614c0 --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 000000000..4f09a2595 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,807 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/js/cryptojs-core.js b/app/src/main/assets/js/cryptojs-core.js new file mode 100644 index 000000000..cd7ac9fab --- /dev/null +++ b/app/src/main/assets/js/cryptojs-core.js @@ -0,0 +1,719 @@ +/* + * CryptoJS v3.1.2 + * https://code.google.com/p/crypto-js + * (c) 2009-2013 by Jeff Mott. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation or other materials provided with the distribution. + * Neither the name CryptoJS nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * CryptoJS core components. + */ +var CryptoJS = CryptoJS || (function (Math, undefined) { + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + function F() {} + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + F.prototype = this; + var subtype = new F(); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init')) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else if (thatWords.length > 0xffff) { + // Copy one word at a time + for (var i = 0; i < thatSigBytes; i += 4) { + thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; + } + } else { + // Copy all words at once + thisWords.push.apply(thisWords, thatWords); + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + for (var i = 0; i < nBytes; i += 4) { + words.push((Math.random() * 0x100000000) | 0); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + var processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; +}(Math)); diff --git a/app/src/main/assets/js/cryptojs/aes.js b/app/src/main/assets/js/cryptojs/aes.js new file mode 100644 index 000000000..016ec6a39 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/aes.js @@ -0,0 +1,35 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, +2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, +q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< +l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), +f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, +m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, +E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ +4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); +(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, +this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, +1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, +b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); +(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, +16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> +8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= +d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); diff --git a/app/src/main/assets/js/cryptojs/hmac-md5.js b/app/src/main/assets/js/cryptojs/hmac-md5.js new file mode 100644 index 000000000..19d257e70 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/hmac-md5.js @@ -0,0 +1,21 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(q,r){var k={},g=k.lib={},p=function(){},t=g.Base={extend:function(b){p.prototype=this;var j=new p;b&&j.mixIn(b);j.hasOwnProperty("init")||(j.init=function(){j.$super.init.apply(this,arguments)});j.init.prototype=j;j.$super=this;return j},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var j in b)b.hasOwnProperty(j)&&(this[j]=b[j]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}}, +n=g.WordArray=t.extend({init:function(b,j){b=this.words=b||[];this.sigBytes=j!=r?j:4*b.length},toString:function(b){return(b||u).stringify(this)},concat:function(b){var j=this.words,a=b.words,l=this.sigBytes;b=b.sigBytes;this.clamp();if(l%4)for(var h=0;h>>2]|=(a[h>>>2]>>>24-8*(h%4)&255)<<24-8*((l+h)%4);else if(65535>>2]=a[h>>>2];else j.push.apply(j,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,j=this.sigBytes;b[j>>>2]&=4294967295<< +32-8*(j%4);b.length=q.ceil(j/4)},clone:function(){var b=t.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var j=[],a=0;a>>2]>>>24-8*(l%4)&255;h.push((m>>>4).toString(16));h.push((m&15).toString(16))}return h.join("")},parse:function(b){for(var a=b.length,h=[],l=0;l>>3]|=parseInt(b.substr(l, +2),16)<<24-4*(l%8);return new n.init(h,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var h=[],l=0;l>>2]>>>24-8*(l%4)&255));return h.join("")},parse:function(b){for(var a=b.length,h=[],l=0;l>>2]|=(b.charCodeAt(l)&255)<<24-8*(l%4);return new n.init(h,a)}},s=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(h){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, +h=g.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new n.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=s.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,h=a.words,l=a.sigBytes,m=this.blockSize,k=l/(4*m),k=b?q.ceil(k):q.max((k|0)-this._minBufferSize,0);b=k*m;l=q.min(4*b,l);if(b){for(var g=0;g>>32-l)+m}function k(a,m,b,j,g,l,k){a=a+(m&j|b&~j)+g+k;return(a<>>32-l)+m}function g(a,m,b,j,g,l,k){a=a+(m^b^j)+g+k;return(a<>>32-l)+m}function p(a,g,b,j,k,l,p){a=a+(b^(g|~j))+k+p;return(a<>>32-l)+g}for(var t=CryptoJS,n=t.lib,v=n.WordArray,u=n.Hasher,n=t.algo,a=[],s=0;64>s;s++)a[s]=4294967296*q.abs(q.sin(s+1))|0;n=n.MD5=u.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(h,m){for(var b=0;16>b;b++){var j=m+b,n=h[j];h[j]=(n<<8|n>>>24)&16711935|(n<<24|n>>>8)&4278255360}var b=this._hash.words,j=h[m+0],n=h[m+1],l=h[m+2],q=h[m+3],t=h[m+4],s=h[m+5],u=h[m+6],v=h[m+7],w=h[m+8],x=h[m+9],y=h[m+10],z=h[m+11],A=h[m+12],B=h[m+13],C=h[m+14],D=h[m+15],c=b[0],d=b[1],e=b[2],f=b[3],c=r(c,d,e,f,j,7,a[0]),f=r(f,c,d,e,n,12,a[1]),e=r(e,f,c,d,l,17,a[2]),d=r(d,e,f,c,q,22,a[3]),c=r(c,d,e,f,t,7,a[4]),f=r(f,c,d,e,s,12,a[5]),e=r(e,f,c,d,u,17,a[6]),d=r(d,e,f,c,v,22,a[7]), +c=r(c,d,e,f,w,7,a[8]),f=r(f,c,d,e,x,12,a[9]),e=r(e,f,c,d,y,17,a[10]),d=r(d,e,f,c,z,22,a[11]),c=r(c,d,e,f,A,7,a[12]),f=r(f,c,d,e,B,12,a[13]),e=r(e,f,c,d,C,17,a[14]),d=r(d,e,f,c,D,22,a[15]),c=k(c,d,e,f,n,5,a[16]),f=k(f,c,d,e,u,9,a[17]),e=k(e,f,c,d,z,14,a[18]),d=k(d,e,f,c,j,20,a[19]),c=k(c,d,e,f,s,5,a[20]),f=k(f,c,d,e,y,9,a[21]),e=k(e,f,c,d,D,14,a[22]),d=k(d,e,f,c,t,20,a[23]),c=k(c,d,e,f,x,5,a[24]),f=k(f,c,d,e,C,9,a[25]),e=k(e,f,c,d,q,14,a[26]),d=k(d,e,f,c,w,20,a[27]),c=k(c,d,e,f,B,5,a[28]),f=k(f,c, +d,e,l,9,a[29]),e=k(e,f,c,d,v,14,a[30]),d=k(d,e,f,c,A,20,a[31]),c=g(c,d,e,f,s,4,a[32]),f=g(f,c,d,e,w,11,a[33]),e=g(e,f,c,d,z,16,a[34]),d=g(d,e,f,c,C,23,a[35]),c=g(c,d,e,f,n,4,a[36]),f=g(f,c,d,e,t,11,a[37]),e=g(e,f,c,d,v,16,a[38]),d=g(d,e,f,c,y,23,a[39]),c=g(c,d,e,f,B,4,a[40]),f=g(f,c,d,e,j,11,a[41]),e=g(e,f,c,d,q,16,a[42]),d=g(d,e,f,c,u,23,a[43]),c=g(c,d,e,f,x,4,a[44]),f=g(f,c,d,e,A,11,a[45]),e=g(e,f,c,d,D,16,a[46]),d=g(d,e,f,c,l,23,a[47]),c=p(c,d,e,f,j,6,a[48]),f=p(f,c,d,e,v,10,a[49]),e=p(e,f,c,d, +C,15,a[50]),d=p(d,e,f,c,s,21,a[51]),c=p(c,d,e,f,A,6,a[52]),f=p(f,c,d,e,q,10,a[53]),e=p(e,f,c,d,y,15,a[54]),d=p(d,e,f,c,n,21,a[55]),c=p(c,d,e,f,w,6,a[56]),f=p(f,c,d,e,D,10,a[57]),e=p(e,f,c,d,u,15,a[58]),d=p(d,e,f,c,B,21,a[59]),c=p(c,d,e,f,t,6,a[60]),f=p(f,c,d,e,z,10,a[61]),e=p(e,f,c,d,l,15,a[62]),d=p(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,g=a.words,b=8*this._nDataBytes,j=8*a.sigBytes;g[j>>>5]|=128<<24-j%32;var k=q.floor(b/ +4294967296);g[(j+64>>>9<<4)+15]=(k<<8|k>>>24)&16711935|(k<<24|k>>>8)&4278255360;g[(j+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(g.length+1);this._process();a=this._hash;g=a.words;for(b=0;4>b;b++)j=g[b],g[b]=(j<<8|j>>>24)&16711935|(j<<24|j>>>8)&4278255360;return a},clone:function(){var a=u.clone.call(this);a._hash=this._hash.clone();return a}});t.MD5=u._createHelper(n);t.HmacMD5=u._createHmacHelper(n)})(Math); +(function(){var q=CryptoJS,r=q.enc.Utf8;q.algo.HMAC=q.lib.Base.extend({init:function(k,g){k=this._hasher=new k.init;"string"==typeof g&&(g=r.parse(g));var p=k.blockSize,q=4*p;g.sigBytes>q&&(g=k.finalize(g));g.clamp();for(var n=this._oKey=g.clone(),v=this._iKey=g.clone(),u=n.words,a=v.words,s=0;s>>2]|=(B[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=B[b>>>2];else d.push.apply(d,B);this.sigBytes+=a;return this},clamp:function(){var a=this.words,d=this.sigBytes;a[d>>>2]&=4294967295<< +32-8*(d%4);a.length=h.ceil(d/4)},clone:function(){var a=l.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var d=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((c>>>4).toString(16));b.push((c&15).toString(16))}return b.join("")},parse:function(a){for(var d=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, +2),16)<<24-4*(f%8);return new m.init(b,d/2)}},w=v.Latin1={stringify:function(a){var d=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var b=a.length,c=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new m.init(c,b)}},k=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(w.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return w.parse(unescape(encodeURIComponent(a)))}}, +u=e.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=new m.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=k.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,c=b.words,f=b.sigBytes,e=this.blockSize,k=f/(4*e),k=a?h.ceil(k):h.max((k|0)-this._minBufferSize,0);a=k*e;f=h.min(4*a,f);if(a){for(var u=0;ub;b++){var a=e+b,d=c[a];c[a]=(d<<8|d>>>24)&16711935|(d<<24|d>>>8)&4278255360}var a=this._hash.words,d=x.words,h=w.words,f=A.words,j=l.words,E=m.words,F=v.words,C,n,p,q,y,D,r,s,t,z;D=C=a[0];r=n=a[1];s=p=a[2];t=q=a[3];z=y=a[4];for(var g,b=0;80>b;b+=1)g=C+c[e+f[b]]|0,g=16>b?g+((n^p^q)+d[0]):32>b?g+((n&p|~n&q)+d[1]):48>b? +g+(((n|~p)^q)+d[2]):64>b?g+((n&q|p&~q)+d[3]):g+((n^(p|~q))+d[4]),g|=0,g=g<>>32-E[b],g=g+y|0,C=y,y=q,q=p<<10|p>>>22,p=n,n=g,g=D+c[e+j[b]]|0,g=16>b?g+((r^(s|~t))+h[0]):32>b?g+((r&t|s&~t)+h[1]):48>b?g+(((r|~s)^t)+h[2]):64>b?g+((r&s|~r&t)+h[3]):g+((r^s^t)+h[4]),g|=0,g=g<>>32-F[b],g=g+z|0,D=z,z=t,t=s<<10|s>>>22,s=r,r=g;g=a[1]+p+t|0;a[1]=a[2]+q+z|0;a[2]=a[3]+y+D|0;a[3]=a[4]+C+r|0;a[4]=a[0]+n+s|0;a[0]=g},_doFinalize:function(){var c=this._data,e=c.words,b=8*this._nDataBytes,a=8*c.sigBytes; +e[a>>>5]|=128<<24-a%32;e[(a+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;c.sigBytes=4*(e.length+1);this._process();c=this._hash;e=c.words;for(b=0;5>b;b++)a=e[b],e[b]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;return c},clone:function(){var c=e.clone.call(this);c._hash=this._hash.clone();return c}});h.RIPEMD160=e._createHelper(j);h.HmacRIPEMD160=e._createHmacHelper(j)})(Math); +(function(){var h=CryptoJS,j=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(c,e){c=this._hasher=new c.init;"string"==typeof e&&(e=j.parse(e));var h=c.blockSize,l=4*h;e.sigBytes>l&&(e=c.finalize(e));e.clamp();for(var m=this._oKey=e.clone(),v=this._iKey=e.clone(),x=m.words,w=v.words,k=0;k>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=k.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, +2),16)<<24-4*(f%8);return new p.init(b,c/2)}},j=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new p.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(j.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return j.parse(unescape(encodeURIComponent(a)))}}, +r=d.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,f=c.sigBytes,d=this.blockSize,e=f/(4*d),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*d;f=g.min(4*a,f);if(a){for(var k=0;ka;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(n<<5|n>>>27)+l+m[a];c=20>a?c+((j&h|~j&g)+1518500249):40>a?c+((j^h^g)+1859775393):60>a?c+((j&h|j&g|h&g)-1894007588):c+((j^h^ +g)-899497514);l=g;g=h;h=j<<30|j>>>2;j=n;n=c}b[0]=b[0]+n|0;b[1]=b[1]+j|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+l|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,g=8*d.sigBytes;e[g>>>5]|=128<<24-g%32;e[(g+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(g+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(l);g.HmacSHA1=d._createHmacHelper(l)})(); +(function(){var g=CryptoJS,l=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=l.parse(d));var g=e.blockSize,k=4*g;d.sigBytes>k&&(d=e.finalize(d));d.clamp();for(var p=this._oKey=d.clone(),b=this._iKey=d.clone(),n=p.words,j=b.words,h=0;h>>2]|=(f[g>>>2]>>>24-8*(g%4)&255)<<24-8*((b+g)%4);else if(65535>>2]=f[g>>>2];else d.push.apply(d,f);this.sigBytes+=a;return this},clamp:function(){var a=this.words,d=this.sigBytes;a[d>>>2]&=4294967295<< +32-8*(d%4);a.length=j.ceil(d/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var d=[],f=0;f>>2]>>>24-8*(b%4)&255;f.push((g>>>4).toString(16));f.push((g&15).toString(16))}return f.join("")},parse:function(a){for(var d=a.length,f=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new r.init(f,d/2)}},n=s.Latin1={stringify:function(a){var d=a.words;a=a.sigBytes;for(var f=[],b=0;b>>2]>>>24-8*(b%4)&255));return f.join("")},parse:function(a){for(var d=a.length,f=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(f,d)}},h=s.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(d){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, +u=e.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var d=this._data,f=d.words,b=d.sigBytes,g=this.blockSize,c=b/(4*g),c=a?j.ceil(c):j.max((c|0)-this._minBufferSize,0);a=c*g;b=j.min(4*a,b);if(a){for(var e=0;en;){var h;a:{h=l;for(var u=j.sqrt(h),t=2;t<=u;t++)if(!(h%t)){h=!1;break a}h=!0}h&&(8>n&&(m[n]=s(j.pow(l,0.5))),r[n]=s(j.pow(l,1/3)),n++);l++}var a=[],c=c.SHA256=p.extend({_doReset:function(){this._hash=new e.init(m.slice(0))},_doProcessBlock:function(d,f){for(var b=this._hash.words,g=b[0],c=b[1],e=b[2],j=b[3],h=b[4],p=b[5],m=b[6],n=b[7],q=0;64>q;q++){if(16>q)a[q]= +d[f+q]|0;else{var k=a[q-15],l=a[q-2];a[q]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[q-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[q-16]}k=n+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&p^~h&m)+r[q]+a[q];l=((g<<30|g>>>2)^(g<<19|g>>>13)^(g<<10|g>>>22))+(g&c^g&e^c&e);n=m;m=p;p=h;h=j+k|0;j=e;e=c;c=g;g=k+l|0}b[0]=b[0]+g|0;b[1]=b[1]+c|0;b[2]=b[2]+e|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+p|0;b[6]=b[6]+m|0;b[7]=b[7]+n|0},_doFinalize:function(){var a=this._data,c=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +c[e>>>5]|=128<<24-e%32;c[(e+64>>>9<<4)+14]=j.floor(b/4294967296);c[(e+64>>>9<<4)+15]=b;a.sigBytes=4*c.length;this._process();return this._hash},clone:function(){var a=p.clone.call(this);a._hash=this._hash.clone();return a}});k.SHA256=p._createHelper(c);k.HmacSHA256=p._createHmacHelper(c)})(Math); +(function(){var j=CryptoJS,k=j.lib.WordArray,c=j.algo,e=c.SHA256,c=c.SHA224=e.extend({_doReset:function(){this._hash=new k.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var c=e._doFinalize.call(this);c.sigBytes-=4;return c}});j.SHA224=e._createHelper(c);j.HmacSHA224=e._createHmacHelper(c)})(); +(function(){var j=CryptoJS,k=j.enc.Utf8;j.algo.HMAC=j.lib.Base.extend({init:function(c,e){c=this._hasher=new c.init;"string"==typeof e&&(e=k.parse(e));var j=c.blockSize,m=4*j;e.sigBytes>m&&(e=c.finalize(e));e.clamp();for(var r=this._oKey=e.clone(),s=this._iKey=e.clone(),l=r.words,n=s.words,h=0;h>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, +u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;gn;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]= +c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math); +(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j>>2]|=(e[p>>>2]>>>24-8*(p%4)&255)<<24-8*((j+p)%4);else if(65535>>2]=e[p>>>2];else b.push.apply(b,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<< +32-8*(b%4);a.length=q.ceil(b/4)},clone:function(){var a=s.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((p>>>4).toString(16));e.push((p&15).toString(16))}return e.join("")},parse:function(a){for(var b=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, +2),16)<<24-4*(j%8);return new t.init(e,b/2)}},g=w.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var b=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new t.init(e,b)}},n=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(g.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return g.parse(unescape(encodeURIComponent(a)))}}, +u=d.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new t.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=n.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,e=b.words,j=b.sigBytes,p=this.blockSize,c=j/(4*p),c=a?q.ceil(c):q.max((c|0)-this._minBufferSize,0);a=c*p;j=q.min(4*a,j);if(a){for(var g=0;gu;u++){t[g+5*n]=(u+1)*(u+2)/2%64;var x=(2*g+3*n)%5,g=n%5,n=x}for(g=0;5>g;g++)for(n=0;5>n;n++)w[g+5*n]=n+5*((2*g+3*n)%5);g=1;for(n=0;24>n;n++){for(var a=x=u=0;7>a;a++){if(g&1){var b=(1<b?x^=1<g;g++)e[g]=s.create();c=c.SHA3=v.extend({cfg:v.cfg.extend({outputLength:512}),_doReset:function(){for(var a=this._state= +[],b=0;25>b;b++)a[b]=new s.init;this.blockSize=(1600-2*this.cfg.outputLength)/32},_doProcessBlock:function(a,b){for(var c=this._state,g=this.blockSize/2,k=0;k>>24)&16711935|(d<<24|d>>>8)&4278255360,l=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360,h=c[k];h.high^=l;h.low^=d}for(g=0;24>g;g++){for(k=0;5>k;k++){for(var f=d=0,m=0;5>m;m++)h=c[k+5*m],d^=h.high,f^=h.low;h=e[k];h.high=d;h.low=f}for(k=0;5>k;k++){h=e[(k+4)%5];d=e[(k+1)%5];l=d.high;m=d.low;d=h.high^ +(l<<1|m>>>31);f=h.low^(m<<1|l>>>31);for(m=0;5>m;m++)h=c[k+5*m],h.high^=d,h.low^=f}for(l=1;25>l;l++)h=c[l],k=h.high,h=h.low,m=t[l],32>m?(d=k<>>32-m,f=h<>>32-m):(d=h<>>64-m,f=k<>>64-m),h=e[w[l]],h.high=d,h.low=f;h=e[0];k=c[0];h.high=k.high;h.low=k.low;for(k=0;5>k;k++)for(m=0;5>m;m++)l=k+5*m,h=c[l],d=e[l],l=e[(k+1)%5+5*m],f=e[(k+2)%5+5*m],h.high=d.high^~l.high&f.high,h.low=d.low^~l.low&f.low;h=c[0];k=r[g];h.high^=k.high;h.low^=k.low}},_doFinalize:function(){var a=this._data, +b=a.words,c=8*a.sigBytes,e=32*this.blockSize;b[c>>>5]|=1<<24-c%32;b[(q.ceil((c+1)/e)*e>>>5)-1]|=128;a.sigBytes=4*b.length;this._process();for(var a=this._state,b=this.cfg.outputLength/8,c=b/8,e=[],g=0;g>>24)&16711935|(l<<24|l>>>8)&4278255360,f=(f<<8|f>>>24)&16711935|(f<<24|f>>>8)&4278255360;e.push(f);e.push(l)}return new d.init(e,b)},clone:function(){for(var a=v.clone.call(this),b=a._state=this._state.slice(0),c=0;25>c;c++)b[c]=b[c].clone();return a}}); +f.SHA3=v._createHelper(c);f.HmacSHA3=v._createHmacHelper(c)})(Math); +(function(){var q=CryptoJS,f=q.enc.Utf8;q.algo.HMAC=q.lib.Base.extend({init:function(c,d){c=this._hasher=new c.init;"string"==typeof d&&(d=f.parse(d));var q=c.blockSize,s=4*q;d.sigBytes>s&&(d=c.finalize(d));d.clamp();for(var t=this._oKey=d.clone(),w=this._iKey=d.clone(),r=t.words,g=w.words,n=0;n>>2]|=(c[b>>>2]>>>24-8*(b%4)&255)<<24-8*((e+b)%4);else if(65535>>2]=c[b>>>2];else g.push.apply(g,c);this.sigBytes+=a;return this},clamp:function(){var C=this.words,g=this.sigBytes;C[g>>>2]&=4294967295<< +32-8*(g%4);C.length=a.ceil(g/4)},clone:function(){var a=l.clone.call(this);a.words=this.words.slice(0);return a},random:function(C){for(var g=[],b=0;b>>2]>>>24-8*(e%4)&255;b.push((c>>>4).toString(16));b.push((c&15).toString(16))}return b.join("")},parse:function(a){for(var b=a.length,c=[],e=0;e>>3]|=parseInt(a.substr(e, +2),16)<<24-4*(e%8);return new u.init(c,b/2)}},x=k.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var c=[],e=0;e>>2]>>>24-8*(e%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],e=0;e>>2]|=(a.charCodeAt(e)&255)<<24-8*(e%4);return new u.init(c,b)}},y=k.Utf8={stringify:function(a){try{return decodeURIComponent(escape(x.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return x.parse(unescape(encodeURIComponent(a)))}}, +$=b.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=new u.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=y.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,l=c.words,e=c.sigBytes,d=this.blockSize,f=e/(4*d),f=b?a.ceil(f):a.max((f|0)-this._minBufferSize,0);b=f*d;e=a.min(4*b,e);if(b){for(var k=0;km;m++)k[m]=a();b=b.SHA512=c.extend({_doReset:function(){this._hash=new l.init([new f.init(1779033703,4089235720),new f.init(3144134277,2227873595),new f.init(1013904242,4271175723),new f.init(2773480762,1595750129),new f.init(1359893119,2917565137),new f.init(2600822924,725511199),new f.init(528734635,4215389547),new f.init(1541459225,327033209)])},_doProcessBlock:function(a,b){for(var c=this._hash.words, +d=c[0],f=c[1],g=c[2],l=c[3],e=c[4],m=c[5],L=c[6],c=c[7],Z=d.high,M=d.low,aa=f.high,N=f.low,ba=g.high,O=g.low,ca=l.high,P=l.low,da=e.high,Q=e.low,ea=m.high,R=m.low,fa=L.high,S=L.low,ga=c.high,T=c.low,r=Z,n=M,F=aa,D=N,G=ba,E=O,W=ca,H=P,s=da,p=Q,U=ea,I=R,V=fa,J=S,X=ga,K=T,t=0;80>t;t++){var z=k[t];if(16>t)var q=z.high=a[b+2*t]|0,h=z.low=a[b+2*t+1]|0;else{var q=k[t-15],h=q.high,v=q.low,q=(h>>>1|v<<31)^(h>>>8|v<<24)^h>>>7,v=(v>>>1|h<<31)^(v>>>8|h<<24)^(v>>>7|h<<25),B=k[t-2],h=B.high,j=B.low,B=(h>>>19|j<< +13)^(h<<3|j>>>29)^h>>>6,j=(j>>>19|h<<13)^(j<<3|h>>>29)^(j>>>6|h<<26),h=k[t-7],Y=h.high,A=k[t-16],w=A.high,A=A.low,h=v+h.low,q=q+Y+(h>>>0>>0?1:0),h=h+j,q=q+B+(h>>>0>>0?1:0),h=h+A,q=q+w+(h>>>0>>0?1:0);z.high=q;z.low=h}var Y=s&U^~s&V,A=p&I^~p&J,z=r&F^r&G^F&G,ja=n&D^n&E^D&E,v=(r>>>28|n<<4)^(r<<30|n>>>2)^(r<<25|n>>>7),B=(n>>>28|r<<4)^(n<<30|r>>>2)^(n<<25|r>>>7),j=u[t],ka=j.high,ha=j.low,j=K+((p>>>14|s<<18)^(p>>>18|s<<14)^(p<<23|s>>>9)),w=X+((s>>>14|p<<18)^(s>>>18|p<<14)^(s<<23|p>>>9))+(j>>>0< +K>>>0?1:0),j=j+A,w=w+Y+(j>>>0>>0?1:0),j=j+ha,w=w+ka+(j>>>0>>0?1:0),j=j+h,w=w+q+(j>>>0>>0?1:0),h=B+ja,z=v+z+(h>>>0>>0?1:0),X=V,K=J,V=U,J=I,U=s,I=p,p=H+j|0,s=W+w+(p>>>0>>0?1:0)|0,W=G,H=E,G=F,E=D,F=r,D=n,n=j+h|0,r=w+z+(n>>>0>>0?1:0)|0}M=d.low=M+n;d.high=Z+r+(M>>>0>>0?1:0);N=f.low=N+D;f.high=aa+F+(N>>>0>>0?1:0);O=g.low=O+E;g.high=ba+G+(O>>>0>>0?1:0);P=l.low=P+H;l.high=ca+W+(P>>>0>>0?1:0);Q=e.low=Q+p;e.high=da+s+(Q>>>0

>>0?1:0);R=m.low=R+I;m.high=ea+U+(R>>>0>>0?1:0); +S=L.low=S+J;L.high=fa+V+(S>>>0>>0?1:0);T=c.low=T+K;c.high=ga+X+(T>>>0>>0?1:0)},_doFinalize:function(){var a=this._data,c=a.words,b=8*this._nDataBytes,d=8*a.sigBytes;c[d>>>5]|=128<<24-d%32;c[(d+128>>>10<<5)+30]=Math.floor(b/4294967296);c[(d+128>>>10<<5)+31]=b;a.sigBytes=4*c.length;this._process();return this._hash.toX32()},clone:function(){var a=c.clone.call(this);a._hash=this._hash.clone();return a},blockSize:32});d.SHA512=c._createHelper(b);d.HmacSHA512=c._createHmacHelper(b)})(); +(function(){var a=CryptoJS,d=a.x64,c=d.Word,b=d.WordArray,d=a.algo,f=d.SHA512,d=d.SHA384=f.extend({_doReset:function(){this._hash=new b.init([new c.init(3418070365,3238371032),new c.init(1654270250,914150663),new c.init(2438529370,812702999),new c.init(355462360,4144912697),new c.init(1731405415,4290775857),new c.init(2394180231,1750603025),new c.init(3675008525,1694076839),new c.init(1203062813,3204075428)])},_doFinalize:function(){var a=f._doFinalize.call(this);a.sigBytes-=16;return a}});a.SHA384= +f._createHelper(d);a.HmacSHA384=f._createHmacHelper(d)})(); +(function(){var a=CryptoJS,d=a.enc.Utf8;a.algo.HMAC=a.lib.Base.extend({init:function(a,b){a=this._hasher=new a.init;"string"==typeof b&&(b=d.parse(b));var f=a.blockSize,l=4*f;b.sigBytes>l&&(b=a.finalize(b));b.clamp();for(var u=this._oKey=b.clone(),k=this._iKey=b.clone(),m=u.words,x=k.words,y=0;y>>2]|=(M[b>>>2]>>>24-8*(b%4)&255)<<24-8*((e+b)%4);else if(65535>>2]=M[b>>>2];else d.push.apply(d,M);this.sigBytes+=a;return this},clamp:function(){var D=this.words,d=this.sigBytes;D[d>>>2]&=4294967295<< +32-8*(d%4);D.length=a.ceil(d/4)},clone:function(){var a=l.clone.call(this);a.words=this.words.slice(0);return a},random:function(D){for(var d=[],b=0;b>>2]>>>24-8*(e%4)&255;b.push((c>>>4).toString(16));b.push((c&15).toString(16))}return b.join("")},parse:function(a){for(var d=a.length,b=[],e=0;e>>3]|=parseInt(a.substr(e, +2),16)<<24-4*(e%8);return new u.init(b,d/2)}},y=k.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var c=[],e=0;e>>2]>>>24-8*(e%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],e=0;e>>2]|=(a.charCodeAt(e)&255)<<24-8*(e%4);return new u.init(c,b)}},z=k.Utf8={stringify:function(a){try{return decodeURIComponent(escape(y.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return y.parse(unescape(encodeURIComponent(a)))}}, +x=b.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=new u.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=z.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(b){var d=this._data,c=d.words,e=d.sigBytes,l=this.blockSize,k=e/(4*l),k=b?a.ceil(k):a.max((k|0)-this._minBufferSize,0);b=k*l;e=a.min(4*b,e);if(b){for(var x=0;xm;m++)k[m]=a();b=b.SHA512=c.extend({_doReset:function(){this._hash=new l.init([new f.init(1779033703,4089235720),new f.init(3144134277,2227873595),new f.init(1013904242,4271175723),new f.init(2773480762,1595750129),new f.init(1359893119,2917565137),new f.init(2600822924,725511199),new f.init(528734635,4215389547),new f.init(1541459225,327033209)])},_doProcessBlock:function(a,b){for(var c=this._hash.words, +f=c[0],j=c[1],d=c[2],l=c[3],e=c[4],m=c[5],N=c[6],c=c[7],aa=f.high,O=f.low,ba=j.high,P=j.low,ca=d.high,Q=d.low,da=l.high,R=l.low,ea=e.high,S=e.low,fa=m.high,T=m.low,ga=N.high,U=N.low,ha=c.high,V=c.low,r=aa,n=O,G=ba,E=P,H=ca,F=Q,Y=da,I=R,s=ea,p=S,W=fa,J=T,X=ga,K=U,Z=ha,L=V,t=0;80>t;t++){var A=k[t];if(16>t)var q=A.high=a[b+2*t]|0,g=A.low=a[b+2*t+1]|0;else{var q=k[t-15],g=q.high,v=q.low,q=(g>>>1|v<<31)^(g>>>8|v<<24)^g>>>7,v=(v>>>1|g<<31)^(v>>>8|g<<24)^(v>>>7|g<<25),C=k[t-2],g=C.high,h=C.low,C=(g>>>19| +h<<13)^(g<<3|h>>>29)^g>>>6,h=(h>>>19|g<<13)^(h<<3|g>>>29)^(h>>>6|g<<26),g=k[t-7],$=g.high,B=k[t-16],w=B.high,B=B.low,g=v+g.low,q=q+$+(g>>>0>>0?1:0),g=g+h,q=q+C+(g>>>0>>0?1:0),g=g+B,q=q+w+(g>>>0>>0?1:0);A.high=q;A.low=g}var $=s&W^~s&X,B=p&J^~p&K,A=r&G^r&H^G&H,ka=n&E^n&F^E&F,v=(r>>>28|n<<4)^(r<<30|n>>>2)^(r<<25|n>>>7),C=(n>>>28|r<<4)^(n<<30|r>>>2)^(n<<25|r>>>7),h=u[t],la=h.high,ia=h.low,h=L+((p>>>14|s<<18)^(p>>>18|s<<14)^(p<<23|s>>>9)),w=Z+((s>>>14|p<<18)^(s>>>18|p<<14)^(s<<23|p>>>9))+(h>>> +0>>0?1:0),h=h+B,w=w+$+(h>>>0>>0?1:0),h=h+ia,w=w+la+(h>>>0>>0?1:0),h=h+g,w=w+q+(h>>>0>>0?1:0),g=C+ka,A=v+A+(g>>>0>>0?1:0),Z=X,L=K,X=W,K=J,W=s,J=p,p=I+h|0,s=Y+w+(p>>>0>>0?1:0)|0,Y=H,I=F,H=G,F=E,G=r,E=n,n=h+g|0,r=w+A+(n>>>0>>0?1:0)|0}O=f.low=O+n;f.high=aa+r+(O>>>0>>0?1:0);P=j.low=P+E;j.high=ba+G+(P>>>0>>0?1:0);Q=d.low=Q+F;d.high=ca+H+(Q>>>0>>0?1:0);R=l.low=R+I;l.high=da+Y+(R>>>0>>0?1:0);S=e.low=S+p;e.high=ea+s+(S>>>0

>>0?1:0);N=j.low=N+D;j.high=$+G+(N>>>0>>0?1:0);O=b.low=O+E;b.high=aa+H+(O>>>0>>0?1:0);P=g.low=P+I;g.high=ba+W+(P>>>0>>0?1:0);Q=e.low=Q+q;e.high=ca+t+(Q>>>0>>0?1:0);R=k.low=R+J;k.high=da+U+(R>>>0>>0?1:0); +S=m.low=S+K;m.high=ea+V+(S>>>0>>0?1:0);T=d.low=T+L;d.high=fa+X+(T>>>0>>0?1:0)},_doFinalize:function(){var a=this._data,c=a.words,d=8*this._nDataBytes,f=8*a.sigBytes;c[f>>>5]|=128<<24-f%32;c[(f+128>>>10<<5)+30]=Math.floor(d/4294967296);c[(f+128>>>10<<5)+31]=d;a.sigBytes=4*c.length;this._process();return this._hash.toX32()},clone:function(){var a=d.clone.call(this);a._hash=this._hash.clone();return a},blockSize:32});c.SHA512=d._createHelper(j);c.HmacSHA512=d._createHmacHelper(j)})(); +(function(){var a=CryptoJS,c=a.x64,d=c.Word,j=c.WordArray,c=a.algo,f=c.SHA512,c=c.SHA384=f.extend({_doReset:function(){this._hash=new j.init([new d.init(3418070365,3238371032),new d.init(1654270250,914150663),new d.init(2438529370,812702999),new d.init(355462360,4144912697),new d.init(1731405415,4290775857),new d.init(2394180231,1750603025),new d.init(3675008525,1694076839),new d.init(1203062813,3204075428)])},_doFinalize:function(){var a=f._doFinalize.call(this);a.sigBytes-=16;return a}});a.SHA384= +f._createHelper(c);a.HmacSHA384=f._createHmacHelper(c)})(); diff --git a/app/src/main/assets/js/cryptojs/sha512.js b/app/src/main/assets/js/cryptojs/sha512.js new file mode 100644 index 000000000..b32746340 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha512.js @@ -0,0 +1,23 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(a,m){var r={},f=r.lib={},g=function(){},l=f.Base={extend:function(a){g.prototype=this;var b=new g;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +p=f.WordArray=l.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=m?b:4*a.length},toString:function(a){return(a||q).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var j=0;j>>2]|=(d[j>>>2]>>>24-8*(j%4)&255)<<24-8*((c+j)%4);else if(65535>>2]=d[j>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var n=this.words,b=this.sigBytes;n[b>>>2]&=4294967295<< +32-8*(b%4);n.length=a.ceil(b/4)},clone:function(){var a=l.clone.call(this);a.words=this.words.slice(0);return a},random:function(n){for(var b=[],d=0;d>>2]>>>24-8*(c%4)&255;d.push((j>>>4).toString(16));d.push((j&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c>>3]|=parseInt(a.substr(c, +2),16)<<24-4*(c%8);return new p.init(d,b/2)}},G=y.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new p.init(d,b)}},fa=y.Utf8={stringify:function(a){try{return decodeURIComponent(escape(G.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return G.parse(unescape(encodeURIComponent(a)))}}, +h=f.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=fa.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(n){var b=this._data,d=b.words,c=b.sigBytes,j=this.blockSize,l=c/(4*j),l=n?a.ceil(l):a.max((l|0)-this._minBufferSize,0);n=l*j;c=a.min(4*n,c);if(n){for(var h=0;hq;q++)y[q]=a();f=f.SHA512=r.extend({_doReset:function(){this._hash=new l.init([new g.init(1779033703,4089235720),new g.init(3144134277,2227873595),new g.init(1013904242,4271175723),new g.init(2773480762,1595750129),new g.init(1359893119,2917565137),new g.init(2600822924,725511199),new g.init(528734635,4215389547),new g.init(1541459225,327033209)])},_doProcessBlock:function(a,f){for(var h=this._hash.words, +g=h[0],n=h[1],b=h[2],d=h[3],c=h[4],j=h[5],l=h[6],h=h[7],q=g.high,m=g.low,r=n.high,N=n.low,Z=b.high,O=b.low,$=d.high,P=d.low,aa=c.high,Q=c.low,ba=j.high,R=j.low,ca=l.high,S=l.low,da=h.high,T=h.low,v=q,s=m,H=r,E=N,I=Z,F=O,W=$,J=P,w=aa,t=Q,U=ba,K=R,V=ca,L=S,X=da,M=T,x=0;80>x;x++){var B=y[x];if(16>x)var u=B.high=a[f+2*x]|0,e=B.low=a[f+2*x+1]|0;else{var u=y[x-15],e=u.high,z=u.low,u=(e>>>1|z<<31)^(e>>>8|z<<24)^e>>>7,z=(z>>>1|e<<31)^(z>>>8|e<<24)^(z>>>7|e<<25),D=y[x-2],e=D.high,k=D.low,D=(e>>>19|k<<13)^ +(e<<3|k>>>29)^e>>>6,k=(k>>>19|e<<13)^(k<<3|e>>>29)^(k>>>6|e<<26),e=y[x-7],Y=e.high,C=y[x-16],A=C.high,C=C.low,e=z+e.low,u=u+Y+(e>>>0>>0?1:0),e=e+k,u=u+D+(e>>>0>>0?1:0),e=e+C,u=u+A+(e>>>0>>0?1:0);B.high=u;B.low=e}var Y=w&U^~w&V,C=t&K^~t&L,B=v&H^v&I^H&I,ha=s&E^s&F^E&F,z=(v>>>28|s<<4)^(v<<30|s>>>2)^(v<<25|s>>>7),D=(s>>>28|v<<4)^(s<<30|v>>>2)^(s<<25|v>>>7),k=p[x],ia=k.high,ea=k.low,k=M+((t>>>14|w<<18)^(t>>>18|w<<14)^(t<<23|w>>>9)),A=X+((w>>>14|t<<18)^(w>>>18|t<<14)^(w<<23|t>>>9))+(k>>>0>> +0?1:0),k=k+C,A=A+Y+(k>>>0>>0?1:0),k=k+ea,A=A+ia+(k>>>0>>0?1:0),k=k+e,A=A+u+(k>>>0>>0?1:0),e=D+ha,B=z+B+(e>>>0>>0?1:0),X=V,M=L,V=U,L=K,U=w,K=t,t=J+k|0,w=W+A+(t>>>0>>0?1:0)|0,W=I,J=F,I=H,F=E,H=v,E=s,s=k+e|0,v=A+B+(s>>>0>>0?1:0)|0}m=g.low=m+s;g.high=q+v+(m>>>0>>0?1:0);N=n.low=N+E;n.high=r+H+(N>>>0>>0?1:0);O=b.low=O+F;b.high=Z+I+(O>>>0>>0?1:0);P=d.low=P+J;d.high=$+W+(P>>>0>>0?1:0);Q=c.low=Q+t;c.high=aa+w+(Q>>>0>>0?1:0);R=j.low=R+K;j.high=ba+U+(R>>>0>>0?1:0);S=l.low= +S+L;l.high=ca+V+(S>>>0>>0?1:0);T=h.low=T+M;h.high=da+X+(T>>>0>>0?1:0)},_doFinalize:function(){var a=this._data,f=a.words,h=8*this._nDataBytes,g=8*a.sigBytes;f[g>>>5]|=128<<24-g%32;f[(g+128>>>10<<5)+30]=Math.floor(h/4294967296);f[(g+128>>>10<<5)+31]=h;a.sigBytes=4*f.length;this._process();return this._hash.toX32()},clone:function(){var a=r.clone.call(this);a._hash=this._hash.clone();return a},blockSize:32});m.SHA512=r._createHelper(f);m.HmacSHA512=r._createHmacHelper(f)})(); diff --git a/app/src/main/assets/js/cryptojs/tripledes.js b/app/src/main/assets/js/cryptojs/tripledes.js new file mode 100644 index 000000000..5ae549a0e --- /dev/null +++ b/app/src/main/assets/js/cryptojs/tripledes.js @@ -0,0 +1,51 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(u,l){var d={},n=d.lib={},p=function(){},s=n.Base={extend:function(a){p.prototype=this;var c=new p;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=n.WordArray=s.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=l?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,m=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var t=0;t>>2]|=(m[t>>>2]>>>24-8*(t%4)&255)<<24-8*((f+t)%4);else if(65535>>2]=m[t>>>2];else c.push.apply(c,m);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=s.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],m=0;m>>2]>>>24-8*(f%4)&255;m.push((t>>>4).toString(16));m.push((t&15).toString(16))}return m.join("")},parse:function(a){for(var c=a.length,m=[],f=0;f>>3]|=parseInt(a.substr(f, +2),16)<<24-4*(f%8);return new q.init(m,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var m=[],f=0;f>>2]>>>24-8*(f%4)&255));return m.join("")},parse:function(a){for(var c=a.length,m=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new q.init(m,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, +r=n.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,m=c.words,f=c.sigBytes,t=this.blockSize,b=f/(4*t),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*t;f=u.min(4*a,f);if(a){for(var e=0;e>>2]>>>24-8*(q%4)&255)<<16|(n[q+1>>>2]>>>24-8*((q+1)%4)&255)<<8|n[q+2>>>2]>>>24-8*((q+2)%4)&255,v=0;4>v&&q+0.75*v>>6*(3-v)&63));if(n=s.charAt(64))for(;d.length%4;)d.push(n);return d.join("")},parse:function(d){var n=d.length,p=this._map,s=p.charAt(64);s&&(s=d.indexOf(s),-1!=s&&(n=s));for(var s=[],q=0,w=0;w< +n;w++)if(w%4){var v=p.indexOf(d.charAt(w-1))<<2*(w%4),b=p.indexOf(d.charAt(w))>>>6-2*(w%4);s[q>>>2]|=(v|b)<<24-8*(q%4);q++}return l.create(s,q)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(u){function l(b,e,a,c,m,f,t){b=b+(e&a|~e&c)+m+t;return(b<>>32-f)+e}function d(b,e,a,c,m,f,t){b=b+(e&c|a&~c)+m+t;return(b<>>32-f)+e}function n(b,e,a,c,m,f,t){b=b+(e^a^c)+m+t;return(b<>>32-f)+e}function p(b,e,a,c,m,f,t){b=b+(a^(e|~c))+m+t;return(b<>>32-f)+e}for(var s=CryptoJS,q=s.lib,w=q.WordArray,v=q.Hasher,q=s.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;q=q.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(r,e){for(var a=0;16>a;a++){var c=e+a,m=r[c];r[c]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360}var a=this._hash.words,c=r[e+0],m=r[e+1],f=r[e+2],t=r[e+3],y=r[e+4],q=r[e+5],s=r[e+6],w=r[e+7],v=r[e+8],u=r[e+9],x=r[e+10],z=r[e+11],A=r[e+12],B=r[e+13],C=r[e+14],D=r[e+15],g=a[0],h=a[1],j=a[2],k=a[3],g=l(g,h,j,k,c,7,b[0]),k=l(k,g,h,j,m,12,b[1]),j=l(j,k,g,h,f,17,b[2]),h=l(h,j,k,g,t,22,b[3]),g=l(g,h,j,k,y,7,b[4]),k=l(k,g,h,j,q,12,b[5]),j=l(j,k,g,h,s,17,b[6]),h=l(h,j,k,g,w,22,b[7]), +g=l(g,h,j,k,v,7,b[8]),k=l(k,g,h,j,u,12,b[9]),j=l(j,k,g,h,x,17,b[10]),h=l(h,j,k,g,z,22,b[11]),g=l(g,h,j,k,A,7,b[12]),k=l(k,g,h,j,B,12,b[13]),j=l(j,k,g,h,C,17,b[14]),h=l(h,j,k,g,D,22,b[15]),g=d(g,h,j,k,m,5,b[16]),k=d(k,g,h,j,s,9,b[17]),j=d(j,k,g,h,z,14,b[18]),h=d(h,j,k,g,c,20,b[19]),g=d(g,h,j,k,q,5,b[20]),k=d(k,g,h,j,x,9,b[21]),j=d(j,k,g,h,D,14,b[22]),h=d(h,j,k,g,y,20,b[23]),g=d(g,h,j,k,u,5,b[24]),k=d(k,g,h,j,C,9,b[25]),j=d(j,k,g,h,t,14,b[26]),h=d(h,j,k,g,v,20,b[27]),g=d(g,h,j,k,B,5,b[28]),k=d(k,g, +h,j,f,9,b[29]),j=d(j,k,g,h,w,14,b[30]),h=d(h,j,k,g,A,20,b[31]),g=n(g,h,j,k,q,4,b[32]),k=n(k,g,h,j,v,11,b[33]),j=n(j,k,g,h,z,16,b[34]),h=n(h,j,k,g,C,23,b[35]),g=n(g,h,j,k,m,4,b[36]),k=n(k,g,h,j,y,11,b[37]),j=n(j,k,g,h,w,16,b[38]),h=n(h,j,k,g,x,23,b[39]),g=n(g,h,j,k,B,4,b[40]),k=n(k,g,h,j,c,11,b[41]),j=n(j,k,g,h,t,16,b[42]),h=n(h,j,k,g,s,23,b[43]),g=n(g,h,j,k,u,4,b[44]),k=n(k,g,h,j,A,11,b[45]),j=n(j,k,g,h,D,16,b[46]),h=n(h,j,k,g,f,23,b[47]),g=p(g,h,j,k,c,6,b[48]),k=p(k,g,h,j,w,10,b[49]),j=p(j,k,g,h, +C,15,b[50]),h=p(h,j,k,g,q,21,b[51]),g=p(g,h,j,k,A,6,b[52]),k=p(k,g,h,j,t,10,b[53]),j=p(j,k,g,h,x,15,b[54]),h=p(h,j,k,g,m,21,b[55]),g=p(g,h,j,k,v,6,b[56]),k=p(k,g,h,j,D,10,b[57]),j=p(j,k,g,h,s,15,b[58]),h=p(h,j,k,g,B,21,b[59]),g=p(g,h,j,k,y,6,b[60]),k=p(k,g,h,j,z,10,b[61]),j=p(j,k,g,h,f,15,b[62]),h=p(h,j,k,g,u,21,b[63]);a[0]=a[0]+g|0;a[1]=a[1]+h|0;a[2]=a[2]+j|0;a[3]=a[3]+k|0},_doFinalize:function(){var b=this._data,e=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;e[c>>>5]|=128<<24-c%32;var m=u.floor(a/ +4294967296);e[(c+64>>>9<<4)+15]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360;e[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(e.length+1);this._process();b=this._hash;e=b.words;for(a=0;4>a;a++)c=e[a],e[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});s.MD5=v._createHelper(q);s.HmacMD5=v._createHmacHelper(q)})(Math); +(function(){var u=CryptoJS,l=u.lib,d=l.Base,n=l.WordArray,l=u.algo,p=l.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:l.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,l){for(var p=this.cfg,v=p.hasher.create(),b=n.create(),u=b.words,r=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:r}),reset:function(){v.reset.call(this);var a=this.cfg,c=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var b=a.createEncryptor;else b=a.createDecryptor,this._minBufferSize=1;this._mode=b.call(a, +this,c&&c.words)},_doProcessBlock:function(a,c){this._mode.processBlock(a,c)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var c=this._process(!0)}else c=this._process(!0),a.unpad(c);return c},blockSize:4});var e=d.CipherParams=n.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(l.format={}).OpenSSL={stringify:function(a){var c=a.ciphertext;a=a.salt;return(a?p.create([1398893684, +1701076831]).concat(a).concat(c):c).toString(q)},parse:function(a){a=q.parse(a);var c=a.words;if(1398893684==c[0]&&1701076831==c[1]){var b=p.create(c.slice(2,4));c.splice(0,4);a.sigBytes-=16}return e.create({ciphertext:a,salt:b})}},a=d.SerializableCipher=n.extend({cfg:n.extend({format:b}),encrypt:function(a,c,b,d){d=this.cfg.extend(d);var l=a.createEncryptor(b,d);c=l.finalize(c);l=l.cfg;return e.create({ciphertext:c,key:b,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,c,b,e){e=this.cfg.extend(e);c=this._parse(c,e.format);return a.createDecryptor(b,e).finalize(c.ciphertext)},_parse:function(a,c){return"string"==typeof a?c.parse(a,this):a}}),l=(l.kdf={}).OpenSSL={execute:function(a,c,b,d){d||(d=p.random(8));a=w.create({keySize:c+b}).compute(a,d);b=p.create(a.words.slice(c),4*b);a.sigBytes=4*c;return e.create({key:a,iv:b,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:l}),encrypt:function(c,b,e,d){d=this.cfg.extend(d);e=d.kdf.execute(e, +c.keySize,c.ivSize);d.iv=e.iv;c=a.encrypt.call(this,c,b,e.key,d);c.mixIn(e);return c},decrypt:function(c,b,e,d){d=this.cfg.extend(d);b=this._parse(b,d.format);e=d.kdf.execute(e,c.keySize,c.ivSize,b.salt);d.iv=e.iv;return a.decrypt.call(this,c,b,e.key,d)}})}(); +(function(){function u(b,a){var c=(this._lBlock>>>b^this._rBlock)&a;this._rBlock^=c;this._lBlock^=c<>>b^this._lBlock)&a;this._lBlock^=c;this._rBlock^=c<c;c++){var d=q[c]-1;a[c]=b[d>>>5]>>>31-d%32&1}b=this._subKeys=[];for(d=0;16>d;d++){for(var f=b[d]=[],l=v[d],c=0;24>c;c++)f[c/6|0]|=a[(w[c]-1+l)%28]<<31-c%6,f[4+(c/6|0)]|=a[28+(w[c+24]-1+l)%28]<<31-c%6;f[0]=f[0]<<1|f[0]>>>31;for(c=1;7>c;c++)f[c]>>>= +4*(c-1)+3;f[7]=f[7]<<5|f[7]>>>27}a=this._invSubKeys=[];for(c=0;16>c;c++)a[c]=b[15-c]},encryptBlock:function(b,a){this._doCryptBlock(b,a,this._subKeys)},decryptBlock:function(b,a){this._doCryptBlock(b,a,this._invSubKeys)},_doCryptBlock:function(e,a,c){this._lBlock=e[a];this._rBlock=e[a+1];u.call(this,4,252645135);u.call(this,16,65535);l.call(this,2,858993459);l.call(this,8,16711935);u.call(this,1,1431655765);for(var d=0;16>d;d++){for(var f=c[d],n=this._lBlock,p=this._rBlock,q=0,r=0;8>r;r++)q|=b[r][((p^ +f[r])&x[r])>>>0];this._lBlock=p;this._rBlock=n^q}c=this._lBlock;this._lBlock=this._rBlock;this._rBlock=c;u.call(this,1,1431655765);l.call(this,8,16711935);l.call(this,2,858993459);u.call(this,16,65535);u.call(this,4,252645135);e[a]=this._lBlock;e[a+1]=this._rBlock},keySize:2,ivSize:2,blockSize:2});d.DES=n._createHelper(r);s=s.TripleDES=n.extend({_doReset:function(){var b=this._key.words;this._des1=r.createEncryptor(p.create(b.slice(0,2)));this._des2=r.createEncryptor(p.create(b.slice(2,4)));this._des3= +r.createEncryptor(p.create(b.slice(4,6)))},encryptBlock:function(b,a){this._des1.encryptBlock(b,a);this._des2.decryptBlock(b,a);this._des3.encryptBlock(b,a)},decryptBlock:function(b,a){this._des3.decryptBlock(b,a);this._des2.encryptBlock(b,a);this._des1.decryptBlock(b,a)},keySize:6,ivSize:2,blockSize:2});d.TripleDES=n._createHelper(s)})(); diff --git a/app/src/main/assets/js/resolvers/amazon/content/contents/code/amazon.js b/app/src/main/assets/js/resolvers/amazon/content/contents/code/amazon.js new file mode 100755 index 000000000..645954e98 --- /dev/null +++ b/app/src/main/assets/js/resolvers/amazon/content/contents/code/amazon.js @@ -0,0 +1,576 @@ +/* Amazon Music resolver for Tomahawk. + * + * Written in 2015 by Creepy Guy In The Corner + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication + * along with this software. If not, see: + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var AmazonResolver = Tomahawk.extend( Tomahawk.Resolver, { + apiVersion: 0.9, + + api_location : 'https://www.amazon.com/', + + logged_in: null, // null, = not yet tried, 0 = pending, 1 = success, 2 = failed + + settings: { + cacheTime: 300, + name: 'Amazon Music', + icon: '../images/icon.png', + weight: 91, + timeout: 8, + user_agent: 'Mozilla/6.0 (X11; Ubuntu; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0' + }, + + getConfigUi: function() { + return { + "widget": Tomahawk.readBase64( "config.ui" ), + fields: [{ + name: "email", + widget: "email_edit", + property: "text" + }, { + name: "password", + widget: "password_edit", + property: "text" + },{ + name: "region", + widget: "region", + property: "currentIndex" + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + id: "email", + type: "textfield", + label: "E-Mail" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + }, + { + id: "region", + type: "dropdown", + label: "Region", + items: [".com", ".de", ".co.uk"], + defaultValue: 0 + } + ], + + newConfigSaved: function(config) { + var that = this; + var changed = + this._email !== config.email || + this._password !== config.password || + this._region != config.region; + + if (changed) { + return this._get(this.api_location + "gp/dmusic/cloudplayer/forceSignOut").then(function(resp){ + that.init(); + amazonCollection.wipe({id: amazonCollection.settings.id}).then(function () { + window.localStorage.removeItem("amzn_collection_version_key"); + that.init(); + }); + }); + } + }, + + testConfig: function (config) { + var that = this; + return that._get(that.api_location + "gp/dmusic/cloudplayer/forceSignOut").then( + function () { + return that._getLoginPromise(config, true).then(function (resp) { + var appConfigRe = /amznMusic.appConfig *?= *?({.*});/g; + if (appConfigRe.exec(resp) != null) { + return Tomahawk.ConfigTestResultType.Success; + } else { + return Tomahawk.ConfigTestResultType.InvalidCredentials; + } + }, function (error) { + return "Internal error."; + }); + }); + }, + + _request: function(url, method, options, use_csrf_headers){ + if (typeof options === 'undefined') + options = {}; + if (!options.hasOwnProperty('headers')) + options.headers = {}; + if (use_csrf_headers) { + options.headers['csrf-token'] = this._appConfig['CSRFTokenConfig']['csrf_token']; + options.headers['csrf-rnd'] = this._appConfig['CSRFTokenConfig']['csrf_rnd']; + options.headers['csrf-ts'] = this._appConfig['CSRFTokenConfig']['csrf_ts']; + } + + options.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/48.0'; + options.headers['Accept-Language'] = 'en-US,en;q=0.5'; + + if (method == 'POST') + return Tomahawk.post( url, options); + else + return Tomahawk.get( url, options); + }, + _post: function (url, options, use_csrf_headers) { + return this._request(url, 'POST', options, use_csrf_headers); + }, + _get: function (url, options, use_csrf_headers) { + return this._request(url, 'GET', options, use_csrf_headers); + }, + + _domains : ['.com', '.de', '.co.uk'], + + init: function() { + var config = this.getUserConfig(); + + this._email = config.email; + this._password = config.password; + this._region = config.region || 0; + + if (!this._email || !this._password) { + Tomahawk.PluginManager.unregisterPlugin("collection", amazonCollection); + Tomahawk.log("Invalid configuration."); + return; + } + + + this.api_location = 'https://www.amazon' + this._domains[this._region] + '/'; + var that = this; + + return this._get(this.api_location + "gp/dmusic/cloudplayer/forceSignOut").then(function(resp){ + return that._login(config); + }); + }, + + _convertTrack2: function (entry) { + var track = { + artist: Tomahawk.htmlDecode(entry.artist.name), + album: Tomahawk.htmlDecode(entry.album.title), + track: Tomahawk.htmlDecode(entry.title), + title: Tomahawk.htmlDecode(entry.title), + + albumpos: entry.trackNum, + discnumber: entry.discNum, + + duration: entry.duration, + + checked: true, + type: "track", + url : 'amzn://track/' + entry.duration + '/ASIN/' + entry.asin + }; + // also has originalReleaseDate with values like 1476921600000 + + track.hint = track.url; + return track; + }, + + _convertTrack: function (entry) { + if (entry.hasOwnProperty('metadata')) + entry = entry.metadata; + var track = { + artist: Tomahawk.htmlDecode(entry.artistName), + albumArtist: Tomahawk.htmlDecode(entry.albumArtistName), + album: Tomahawk.htmlDecode(entry.albumName), + track: Tomahawk.htmlDecode(entry.title), + title: Tomahawk.htmlDecode(entry.title), + + albumpos: entry.trackNum, + discnumber: entry.discNum, + + duration: entry.duration, + + checked: true, + size: entry.size, + bitrate: entry.bitrate, + type: "track" + }; + + if(entry.albumReleaseDate) + { + track['releaseyear'] = entry.albumReleaseDate.split('-')[0]; + track['year'] = entry.albumReleaseDate.split('-')[0]; + } + + if (entry.purchased === 'true' || entry.uploaded === 'true') + track.url = 'amzn://track/' + entry.duration + '/COID/' + entry.objectId; + else + track.url = 'amzn://track/' + entry.duration + '/ASIN/' + entry.asin; + + track.hint = track.url; + return track; + }, + + search: function (params) { + if (!this.logged_in) { + return this._defer(this.search, params, this); + } else if (this.logged_in === 2) { + throw new Error('Failed login, cannot search.'); + } + + var that = this; + + + //Just a guess, not sure how to check if haz prime music + if (that._appConfig.featureController.hawkfireAccess == 1) + { + return that._post(that.api_location + "clientbuddy/compartments/eeb70a31c77c4ecd/handlers/search", { + data: { + "keywords" : params.query, + "offset" : 0, + "count" : 100, + "marketplaceId" : that._appConfig['cirrus']['marketplaceId'], + "features" : ["musicSubscription"], + "isMusicSubscription" : true, + "primeOnly" : false, + "requestUiContentDeliveredMetrics" : true, + "sslMedia" : true, + "types" : ["track" /* "album", "artist", "station", "playlist" */ ] + }, + dataFormat: 'json', + headers : { + "x-amzn-cb-deviceid" : that._appConfig.deviceId, + "x-amzn-cb-devicetype" : that._appConfig.deviceType + } + }, false).then( function (response) { + return response.trackList.map(that._convertTrack2, that); + }); + } + else if (that._appConfig.featureController.robin == 1) + { + //I have no idea where this URL comes from yet + //This is to search 'Prime Music' + return that._post(that.api_location + "clientbuddy/compartments/32f93572142e8f7c/handlers/search", { + data: { + "keywords" : params.query, + "marketplaceId" : that._appConfig['cirrus']['marketplaceId'] + }, + dataFormat: 'json' + }, true).then( function (response) { + return response.tracks.filter(function(track) { + return track.isPrime; + }).map(that._convertTrack, that); + }); + } else { + return []; + } + }, + + resolve: function (params) { + var query = [ params.artist, params.album, params.track ].join(' '); + + return this.search({query:query}); + }, + + _debugPrint: function (obj, spaces) { + spaces = spaces || ''; + + var str = ''; + for (var key in obj) { + if (typeof obj[key] === "object") { + var b = ["{", "}"]; + if (obj[key].constructor == Array) { + b = ["[", "]"]; + } + str += spaces+key+": "+b[0]+"\n"+this._debugPrint(obj[key], spaces+' ')+"\n"+spaces+b[1]+'\n'; + } else { + str += spaces+key+": "+obj[key]+"\n"; + } + } + if (spaces != '') { + return str; + } else { + str.split('\n').map(Tomahawk.log, Tomahawk); + } + }, + + getStreamUrl: function(params) { + return this._getStreamUrlPromise(params.url).then(function (streamUrl){ + return {url: streamUrl}; + }).catch(Tomahawk.log); + }, + + _parseUrn: function (urn) { + //amzn://track/' + entry.duration + '/ASIN/' + entry.asin + var match = urn.match( /^amzn:\/\/([a-z]+)\/([0-9]+)\/(ASIN|COID)\/(.+)$/ ); + if (!match) return null; + + return { + type: match[ 1 ], + duraion: match[ 2 ], + idType: match[ 3 ], + id: match[ 4 ] + }; + }, + + _getStreamUrlPromise: function (urn) { + if (!this.logged_in) { + return this._defer(this.getStreamUrl, [urn], this); + } else if (this.logged_in === 2) { + throw new Error('Failed login, cannot getStreamUrl.'); + } + + var parsedUrn = this._parseUrn( urn ); + + if (!parsedUrn || parsedUrn.type != 'track') { + Tomahawk.log( "Failed to get stream. Couldn't parse '" + urn + "'" ); + return; + } + + + var _headers = { + 'X-Amz-Target' : 'com.amazon.digitalmusiclocator.DigitalMusicLocatorServiceExternal.getRestrictedStreamingURL', + 'Content-Encoding': 'amz-1.0' + }; + + var request = { + "appMetadata": { + "https": "true" + }, + "bitRate": "HIGH", + "clientMetadata": { + "clientId": "WebCP" + }, + "contentDuration": parsedUrn.duration, + "contentId": { + "identifier": parsedUrn.id, + "identifierType": parsedUrn.idType + }, + "customerId": this._appConfig['customerId'], + "deviceToken": { + "deviceId": this._appConfig['deviceId'], + "deviceTypeId": this._appConfig['deviceType'] + } + }; + + + return this._post(this.api_location + "dmls/", { + data: request, + dataFormat: 'json', + headers: _headers + }, true).then( function (response) { + Tomahawk.log(JSON.stringify(response)); + if (! response.contentResponse.urlList) + throw new Error( response.contentResponse.statusMessage ); + return response.contentResponse.urlList[0]; + }); + }, + + _defer: function (callback, args, scope) { + if (typeof this._loginPromise !== 'undefined' && 'then' in this._loginPromise) { + args = args || []; + scope = scope || this; + Tomahawk.log('Deferring action with ' + args.length + ' arguments.'); + return this._loginPromise.then(function () { + Tomahawk.log('Performing deferred action with ' + args.length + ' arguments.'); + callback.call(scope, args); + }); + } + }, + + _getLoginPromise: function (config, isTestingConfig) { + var that = this; + var options = { + isTestingConfig: isTestingConfig + }; + return this._get(this.api_location + "cloudplayer", options).then( + function (resp) { + if (resp.indexOf('amznMusic.appConfig') !== -1 ) + { + //We are already logged in + return resp; + } + else + { + var myRE = /input type=.*? name="([^"]+)" value="([^"]+)"/g; + + var match = myRE.exec(resp); + var params = {}; + while (match != null) { + params[match[1]] = match[2]; + match = myRE.exec(resp); + } + params['email'] = config.email.trim(); + params['password'] = config.password.trim(); + params['create'] = '0'; + var actionRe = /action="([^"]+)"/g ; + var url = actionRe.exec(resp)[1]; + var tokenRE = /token...([A-F0-9]+)/g ; + var token = tokenRE.exec(resp)[1]; + var options = { + data: params, + headers : { 'Referer' : 'https://www.amazon' + that._domains[that._region] + '/ap/signin?_encoding=UTF8&accountStatusPolicy=P1&openid.assoc_handle=usflex&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon' + that._domains[that._region] + '%3A443%2Fgp%2Fredirect.html%3F_encoding%3DUTF8%26location%3Dhttps%253A%252F%252Fmusic.amazon' + that._domains[that._region] + '%253Fref_%253Ddm_wcp_sfso%26source%3Dstandards%26token%3D' + token + '%23&pageId=amzn_cpweb&showRmrMe=1' } + }; + if (isTestingConfig) { + options.isTestingConfig = true; + } + return that._post(url, options); + } + }, + function (error) { + Tomahawk.log(JSON.stringify(error)); + Tomahawk.log('Error getting login page'); + that.logged_in = 2; + } + ); + }, + + _login: function (config) { + // If a login is already in progress don't start another! + if (this.logged_in === 0) return; + this.logged_in = 0; + + var that = this; + + this._loginPromise = this._getLoginPromise(config, false).then( + function (resp) { + var appConfigRe = /amznMusic.appConfig *?= *?({.*});/g; + that._appConfig = JSON.parse(appConfigRe.exec(resp)[1]); + that.logged_in = 1; + that.api_location = 'https://' + that._appConfig['serverName'] + '/'; + amazonCollection.settings['description'] = that._appConfig['customerName']; + Tomahawk.PluginManager.registerPlugin("collection", amazonCollection); + that._checkForLibraryUpdates().then(function(){ + amazonCollection.addTracks({ + id: amazonCollection.settings.id, + tracks: [] + }); + }, function (error) { + Tomahawk.PluginManager.unregisterPlugin("collection", amazonCollection); + Tomahawk.log("Failed updating Library:" + error); + }); + Tomahawk.log(that.settings.name + " successfully logged in."); + }, + function (error) { + Tomahawk.log(that.settings.name + " failed login: " + JSON.stringify(error)); + + delete that._appConfig; + + that.logged_in = 2; + } + ).catch(function(error) { + Tomahawk.log(that.settings.name + " failed login: " + JSON.stringify(error)); + that.logged_in = 2; + }); + return this._loginPromise; + }, + //Collection/Library + // + _checkForLibraryUpdates: function() { + var that = this; + //Check for library updates every 15 Minutes + setTimeout(function(){that._checkForLibraryUpdates()}, 1000 * 60 * 15); + var currentVersion = Tomahawk.localStorage.getItem("amzn_collection_version_key"); + var _query = { + 'caller' : 'checkServerChange', + 'Operation' : 'getGlobalLastUpdatedDate', + 'ContentType' : 'JSON', + 'customerInfo.customerId' : this._appConfig['customerId'], + 'customerInfo.deviceId' : this._appConfig['deviceId'], + 'customerInfo.deviceType' : this._appConfig['deviceType'] + }; + return this._post(this.api_location + 'cirrus/', { + data: _query + }, true).then( function (response) { + var serverVersion = response.getGlobalLastUpdatedDateResponse.getGlobalLastUpdatedDateResult.date.toString(); + if( currentVersion != serverVersion ) { + Tomahawk.log('Server-side library updated, syncing'); + amazonCollection.wipe({id: amazonCollection.settings.id}).then(function () { + Tomahawk.localStorage.removeItem("amzn_collection_version_key"); + that._getLibraryTracks().then(function(tracks) { + amazonCollection.addTracks({ + id: amazonCollection.settings.id, + tracks: tracks + }).then(function () { + Tomahawk.log("Updated Library to version with date:" + serverVersion); + Tomahawk.localStorage.setItem("amzn_collection_version_key", serverVersion); + }); + }); + }); + } else { + Tomahawk.log('Library up-to-date'); + } + }); + }, + + _getLibraryTracks: function(previousResults, nextResultsToken) { + var that = this; + if (!previousResults) + previousResults = []; + if (!nextResultsToken) + nextResultsToken = ''; + var _query = { + 'ContentType' : 'JSON', + 'Operation' : 'searchLibrary', + 'albumArtUrlsRedirects' : 'false', + 'caller' : 'getServerSongs', + 'countOnly': 'false', + 'customerInfo.customerId' : this._appConfig['customerId'], + 'customerInfo.deviceId' : this._appConfig['deviceId'], + 'customerInfo.deviceType' : this._appConfig['deviceType'], + 'distinctOnly' : 'false', + 'maxResults' : '1000', + 'nextResultsToken' : nextResultsToken, + 'searchCriteria.member.1.attributeName' : 'keywords', + 'searchCriteria.member.1.attributeValue' : '', + 'searchCriteria.member.1.comparisonType' : 'LIKE', + 'searchCriteria.member.2.attributeName' : 'assetType', + 'searchCriteria.member.2.attributeValue' : 'AUDIO', + 'searchCriteria.member.2.comparisonType' : 'EQUALS', + 'searchCriteria.member.3.attributeName' : 'status', + 'searchCriteria.member.3.attributeValue' : 'AVAILABLE', + 'searchCriteria.member.3.comparisonType' : 'EQUALS', + 'searchReturnType' : 'TRACKS', + 'selectedColumns.member.1' : 'albumArtistName', + 'selectedColumns.member.10' : 'title', + 'selectedColumns.member.11' : 'status', + 'selectedColumns.member.12' : 'trackStatus', + 'selectedColumns.member.13' : 'extension', + 'selectedColumns.member.14' : 'asin', + 'selectedColumns.member.15' : 'primeStatus', + 'selectedColumns.member.2' : 'albumName', + 'selectedColumns.member.3' : 'artistName', + 'selectedColumns.member.4' : 'assetType', + 'selectedColumns.member.5' : 'duration', + 'selectedColumns.member.6' : 'objectId', + 'selectedColumns.member.7' : 'sortAlbumArtistName', + 'selectedColumns.member.8' : 'sortAlbumName', + 'selectedColumns.member.9' : 'sortArtistName', + 'sortCriteriaList' : '', + 'sortCriteriaList.member.1.sortColumn' : 'sortTitle', + 'sortCriteriaList.member.1.sortType' : 'ASC' + }; + return that._post(this.api_location + 'cirrus/', { + data: _query + }, true).then(function(response) { + previousResults = previousResults.concat(response.searchLibraryResponse.searchLibraryResult.searchReturnItemList.map(that._convertTrack)); + nextResultsToken = response.searchLibraryResponse.searchLibraryResult.nextResultsToken; + if (null === nextResultsToken) + return previousResults; + return that._getLibraryTracks(previousResults, nextResultsToken); + }); + } +}); + +var amazonCollection = Tomahawk.extend(Tomahawk.Collection, { + resolver: AmazonResolver, + settings: { + id: "amazon", + prettyname: "Amazon Music Library", + iconfile: "contents/images/icon.png" + } +}); +Tomahawk.resolver.instance = AmazonResolver; diff --git a/app/src/main/assets/js/resolvers/amazon/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/amazon/content/contents/code/config.ui new file mode 100644 index 000000000..8cdfd2af7 --- /dev/null +++ b/app/src/main/assets/js/resolvers/amazon/content/contents/code/config.ui @@ -0,0 +1,90 @@ + + + Form + + + + 0 + 0 + 390 + 120 + + + + + 250 + 120 + + + + Form + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Email + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Password + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + Region + + + + + + + + .com + + + + + .de + + + + + .co.uk + + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/amazon/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/amazon/content/contents/images/icon.png new file mode 100644 index 000000000..847121945 Binary files /dev/null and b/app/src/main/assets/js/resolvers/amazon/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconBackground.png new file mode 100644 index 000000000..70c867cc6 Binary files /dev/null and b/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconWhite.png new file mode 100644 index 000000000..576a12142 Binary files /dev/null and b/app/src/main/assets/js/resolvers/amazon/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/amazon/content/metadata.json b/app/src/main/assets/js/resolvers/amazon/content/metadata.json new file mode 100644 index 000000000..96a96f508 --- /dev/null +++ b/app/src/main/assets/js/resolvers/amazon/content/metadata.json @@ -0,0 +1,23 @@ +{ + "name": "Amazon Music", + "pluginName": "amazon", + "author": "Creepy Guy In The Corner", + "email": "", + "version": "0.0.10", + "website": "http://gettomahawk.com", + "description": "Streams music from Amazon Music", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/amazon.js", + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "scripts": [], + "resources": [ + "contents/code/config.ui" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache-icon.png b/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache-icon.png new file mode 100644 index 000000000..3d7d97fdb Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache-icon.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache.js b/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache.js new file mode 100644 index 000000000..466ad6f99 --- /dev/null +++ b/app/src/main/assets/js/resolvers/ampache/content/contents/code/ampache.js @@ -0,0 +1,501 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2011, Dominik Schmidt + * Copyright 2011, Leo Franchi + * Copyright 2013, Teo Mrnjavac + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var AmpacheResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + _ready: false, + + settings: { + name: 'Ampache', + icon: 'ampache-icon.png', + weight: 85, + timeout: 5, + limit: 10 + }, + + getConfigUi: function () { + var uiData = Tomahawk.readBase64("config.ui"); + return { + + "widget": uiData, + fields: [{ + name: "server", + widget: "serverLineEdit", + property: "text" + }, { + name: "username", + widget: "usernameLineEdit", + property: "text" + }, { + name: "password", + widget: "passwordLineEdit", + property: "text" + }], + images: [{ + "owncloud.png": Tomahawk.readBase64("owncloud.png") + }, { + "ampache.png": Tomahawk.readBase64("ampache.png") + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + id: "server", + type: "textfield", + label: "Server URL", + defaultValue: "http://localhost/ampache" + }, + { + id: "username", + type: "textfield", + label: "Username" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + } + ], + + newConfigSaved: function (newConfig) { + if ((newConfig.username != this.username) || (newConfig.password != this.password) + || (newConfig.server != this.server)) { + Tomahawk.log("Invalidating cache"); + var that = this; + ampacheCollection.wipe({id: ampacheCollection.settings.id}).then(function () { + window.localStorage.removeItem("ampache_last_cache_update"); + that.init(); + }); + } + }, + + init: function () { + var that = this; + + this._ready = false; + + if (!this.element) { + this.element = document.createElement('div'); + } + + // check resolver is properly configured + var userConfig = this.getUserConfig(); + if (!userConfig.username || !userConfig.password || !userConfig.server) { + Tomahawk.log("Ampache Resolver not properly configured!"); + return; + } + + this._sanitizeConfig(userConfig); + this.username = userConfig.username; + this.password = userConfig.password; + this.server = userConfig.server; + + return this._login(this.username, this.password, this.server).then(function () { + if (that.auth) { + that._ensureCollection(); + } + }); + }, + + _ensureCollection: function () { + var that = this; + + return ampacheCollection.revision({ + id: ampacheCollection.settings.id + }).then(function (result) { + var lastCollectionUpdate = window.localStorage["ampache_last_collection_update"]; + if (lastCollectionUpdate && lastCollectionUpdate == result) { + Tomahawk.log("Collection database has not been changed since last time."); + var add; + if (window.localStorage["ampache_last_cache_update"]) { + var date = new Date(parseInt(window.localStorage["ampache_last_cache_update"])); + add = date.toISOString(); + } + return that._fetchAndStoreCollection(add); + } else { + Tomahawk.log("Collection database has been changed. Wiping and re-fetching..."); + return ampacheCollection.wipe({ + id: ampacheCollection.settings.id + }).then(function () { + return that._fetchAndStoreCollection(); + }); + } + }); + }, + + _fetchAndStoreCollection: function (add) { + var that = this; + + if (!this._requestPromise) { + Tomahawk.log("Checking if collection needs to be updated"); + var time = Date.now(); + + var settings = { + offset: 0, + limit: 1000000 // EHRM. + }; + if (add) { + settings.add = add; + } + this._requestPromise = this._apiCall("songs", settings).then(function (xmlDoc) { + var songs; + var songElements = xmlDoc.getElementsByTagName("song")[0]; + if (songElements !== undefined && songElements.childNodes.length > 0) { + songs = xmlDoc.getElementsByTagName("song"); + } + Tomahawk.PluginManager.registerPlugin("collection", ampacheCollection); + if (songs && songs.length > 0) { + Tomahawk.log("Collection needs to be updated"); + + var tracks = that._parseSongResponse(xmlDoc); + ampacheCollection.addTracks({ + id: ampacheCollection.settings.id, + tracks: tracks + }).then(function (newRevision) { + Tomahawk.log("Updated cache in " + (Date.now() - time) + "ms"); + window.localStorage["ampache_last_cache_update"] = Date.now(); + window.localStorage["ampache_last_collection_update"] = newRevision; + }); + } else { + Tomahawk.log("Collection doesn't need to be updated"); + + ampacheCollection.addTracks({ + id: ampacheCollection.settings.id, + tracks: [] + }); + } + }, function (xhr) { + Tomahawk.log("Tomahawk.get failed: " + xhr.status + " - " + + xhr.statusText + " - " + xhr.responseText); + }).finally(function () { + that._requestPromise = undefined; + }); + } + return this._requestPromise; + }, + + testConfig: function (config) { + var that = this; + + this._sanitizeConfig(config); + + return this._login(config.username, config.password, config.server) + .then(function (response) { + if (!that.auth) { + Tomahawk.log("auth failed!"); + var error = response.getElementsByTagName("error")[0]; + if (typeof error != 'undefined' && error.getAttribute("code") == "403") { + return TomahawkConfigTestResultType.InvalidAccount; + } else { + return TomahawkConfigTestResultType.InvalidCredentials; + } + } else { + return TomahawkConfigTestResultType.Success; + } + }, function () { + return TomahawkConfigTestResultType.CommunicationError; + }); + }, + + _sanitizeConfig: function (config) { + if (!config.server) { + config.server = "http://localhost/ampache"; + } else { + if (config.server.search(".*:\/\/") < 0) { + // couldn't find a proper protocol, so we default to "http://" + config.server = "http://" + config.server; + } + config.server = config.server.trim(); + } + + return config; + }, + + _handshake: function (username, password, server) { + var time = Tomahawk.timestamp(); + var key, passphrase; + if (typeof CryptoJS !== "undefined" && typeof CryptoJS.SHA256 == "function") { + key = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex); + passphrase = CryptoJS.SHA256(time + key).toString(CryptoJS.enc.Hex); + } else { + key = Tomahawk.sha256(password); + passphrase = Tomahawk.sha256(time + key); + } + + var params = {}; + params.user = username; + params.timestamp = time; + params.version = 350001; + params.auth = passphrase; + + return this._apiCallBase(server, 'handshake', + params).then(this._parseHandshakeResult); + }, + + _parseHandshakeResult: function (xmlDoc) { + var roots = xmlDoc.getElementsByTagName("root"); + var auth = roots[0] === undefined ? false : Tomahawk.valueForSubNode(roots[0], "auth"); + if (!auth) { + Tomahawk.log("INVALID HANDSHAKE RESPONSE!"); + return xmlDoc; + } + + Tomahawk.log("New auth token: " + auth); + var pingInterval = parseInt(roots[0] === undefined ? 0 : Tomahawk.valueForSubNode(roots[0], + "session_length")) * 1000; + var trackCount = roots[0] === undefined ? (-1) : Tomahawk.valueForSubNode(roots[0], + "songs"); + + return { + auth: auth, + trackCount: trackCount > -1 ? parseInt(trackCount) : trackCount, + pingInterval: pingInterval + }; + }, + + _login: function (username, password, server) { + var that = this; + return this._handshake(username, password, server).then(function (result) { + that.auth = result.auth; + that.trackCount = result.trackCount; + + Tomahawk.log("Ampache Resolver properly initialised!"); + that._ready = true; + + // FIXME: the old timer should be cancelled ... + if (result.pingInterval) { + window.setInterval(that._ping, result.pingInterval - 60); + } + return result; + }); + }, + + _apiCallBase: function (serverUrl, action, params) { + params = params || {}; + params.action = action; + + var options = { + url: serverUrl.replace(/\/$/, "") + "/server/xml.server.php", + data: params + }; + + return Tomahawk.get(options); + }, + + _apiCall: function (action, params) { + if (!this.auth) { + throw new Error("Not authed, can't do api call"); + } + + params = params || {}; + params.auth = this.auth; + + var that = this; + return this._apiCallBase(this.server, action, params).then(function (xmlDoc) { + var error = xmlDoc.getElementsByTagName("error")[0]; + if (typeof error != 'undefined' && error.getAttribute("code") == "401") //session expired + { + Tomahawk.log("Let's reauth for: " + action); + return that._login(that.username, that.password, that.server).then(function () { + return that._apiCallBase(action, params); + }, function () { + throw new Error("Could not renew session."); + }); + } + + return xmlDoc; + }); + }, + + _ping: function () { + this._apiCall('ping').then(function () { + Tomahawk.log('Ping succeeded.'); + }, function () { + Tomahawk.log('Ping failed.'); + }); + }, + + _decodeEntity: function (str) { + this.element.innerHTML = str; + return this.element.textContent; + }, + + _parseSongResponse: function (xmlDoc) { + var results = []; + // check the response + var songElements = xmlDoc.getElementsByTagName("song")[0]; + if (songElements !== undefined && songElements.childNodes.length > 0) { + var songs = xmlDoc.getElementsByTagName("song"); + + // walk through the results and store it in 'results' + for (var i = 0; i < songs.length; i++) { + var song = songs[i]; + + results.push({ + artist: this._decodeEntity(Tomahawk.valueForSubNode(song, "artist")), + album: this._decodeEntity(Tomahawk.valueForSubNode(song, "album")), + track: this._decodeEntity(Tomahawk.valueForSubNode(song, "title")), + albumpos: Tomahawk.valueForSubNode(song, "track"), + //result.year = 0;//valueForSubNode(song, "year"); + source: this.settings.name, + url: "ampache://track/" + song.getAttribute("id"), + //mimetype: valueForSubNode(song, "mime"), //FIXME what's up here? it was there before :\ + //result.bitrate = valueForSubNode(song, "title"); + size: Tomahawk.valueForSubNode(song, "size"), + duration: Tomahawk.valueForSubNode(song, "time"), + score: Tomahawk.valueForSubNode(song, "rating") + }); + } + } + return results; + }, + + getStreamUrl: function (params) { + var url = params.url; + + var settings = { + filter: url.replace("ampache://track/", "") + }; + + return this._apiCall("song", settings).then(function (xmlDoc) { + // check the response + var songs = xmlDoc.getElementsByTagName("song"); + if (songs[0] !== undefined && songs[0].childNodes.length > 0) { + return { + url: Tomahawk.valueForSubNode(songs[0], "url") + } + } else { + throw new Error("Wasn't able to get streaming url for song " + settings.filter); + } + }); + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + return this.search({query: artist + " " + track}); + }, + + search: function (params) { + var query = params.query; + + if (!this._ready) { + return; + } + + params = { + filter: query, + limit: this.settings.limit + }; + + var that = this; + return this._apiCall("search_songs", params).then(function (xmlDoc) { + return that._parseSongResponse(xmlDoc); + }); + } + +}); + +Tomahawk.resolver.instance = AmpacheResolver; + +var ampacheCollection = Tomahawk.extend(Tomahawk.Collection, { + settings: { + id: "ampache", + prettyname: "Ampache", + description: AmpacheResolver.getUserConfig() + ? AmpacheResolver._sanitizeConfig(AmpacheResolver.getUserConfig()).server + .replace(/^http:\/\//, "") + .replace(/^https:\/\//, "") + .replace(/\/$/, "") + .replace(/\/remote.php\/ampache/, "") + : "", + iconfile: "contents/images/icon.png", + trackcount: AmpacheResolver.trackCount + } +}); + +/* + * TEST ENVIRONMENT + */ + +/*TomahawkResolver.getUserConfig = function() { + return { + username: "domme", + password: "foo", + ampache: "http://owncloud.lo/ampache" + //ampache: "http://owncloud.lo/apps/media" + }; + };*/ +// +// var resolver = Tomahawk.resolver.instance; +// +// +// // configure tests +// var search = { +// filter: "I Fell" +// }; +// +// var resolve = { +// artist: "The Aquabats!", +// title: "I Fell Asleep On My Arm" +// }; +// // end configure +// +// +// +// +//tests +//resolver.init(); +// +// // test search +// //Tomahawk.log("Search for: " + search.filter ); +// var response1 = resolver.search( 1234, search.filter ); +// //Tomahawk.dumpResult( response1 ); +// +// // test resolve +// //Tomahawk.log("Resolve: " + resolve.artist + " - " + resolve.album + " - " + resolve.title ); +// var response2 = resolver.resolve( 1235, resolve.artist, resolve.album, resolve.title ); +// //Tomahawk.dumpResult( response2 ); +// Tomahawk.log("test"); +// n = 0; +// var items = resolver.getArtists( n ).results; +// for(var i=0;i + + Form + + + + 0 + 0 + 447 + 318 + + + + Form + + + + + + + + + ampache.png + + + Qt::AlignCenter + + + + + + + + + + owncloud.png + + + Qt::AlignCenter + + + + + + + + false + + + + <html><head/><body><p>For ownCloud instances, the Server URL is<br/>http://<span style=" color:#585858;">ownCloud base url</span>/remote.php/ampache</p></body></html> + + + Qt::AlignCenter + + + + + + + + + Server URL: + + + + + + + + + + Username: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud-icon.png b/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud-icon.png new file mode 100644 index 000000000..83493db21 Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud-icon.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud.png b/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud.png new file mode 100644 index 000000000..73269c028 Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/code/owncloud.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/ampache/content/contents/images/icon.png new file mode 100644 index 000000000..3d7d97fdb Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconBackground.png new file mode 100644 index 000000000..82d470755 Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconWhite.png new file mode 100644 index 000000000..52a210431 Binary files /dev/null and b/app/src/main/assets/js/resolvers/ampache/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/ampache/content/metadata.json b/app/src/main/assets/js/resolvers/ampache/content/metadata.json new file mode 100644 index 000000000..4a7b10947 --- /dev/null +++ b/app/src/main/assets/js/resolvers/ampache/content/metadata.json @@ -0,0 +1,27 @@ +{ + "name": "Ampache", + "pluginName": "ampache", + "author": "Dominik, Leo, Teo and Enno", + "email": "teo@kde.org", + "version": "0.4.3", + "website": "http://gettomahawk.com", + "description": "Connects to an Ampache or ownCloud server and resolves tracks.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/ampache.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/config.ui", + "contents/code/ampache-icon.png", + "contents/code/ampache.png", + "contents/code/owncloud-icon.png", + "contents/code/owncloud.png" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/code/beets-icon.png b/app/src/main/assets/js/resolvers/beets/content/contents/code/beets-icon.png new file mode 100644 index 000000000..b405f2a78 Binary files /dev/null and b/app/src/main/assets/js/resolvers/beets/content/contents/code/beets-icon.png differ diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/code/beets.js b/app/src/main/assets/js/resolvers/beets/content/contents/code/beets.js new file mode 100644 index 000000000..7b87272d6 --- /dev/null +++ b/app/src/main/assets/js/resolvers/beets/content/contents/code/beets.js @@ -0,0 +1,241 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Adrian Sampson + * Copyright 2013, Uwe L. Korn + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var BeetsResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'beets', + icon: 'beets-icon.png', + weight: 95, + timeout: 5 + }, + + // Configuration. + getConfigUi: function () { + var uiData = Tomahawk.readBase64("config.ui"); + return { + "widget": uiData, + "fields": [{ + name: "server", + widget: "serverField", + property: "text" + }, { + name: "username", + widget: "usernameField", + property: "text" + }, { + name: "password", + widget: "passwordField", + property: "text" + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + id: "server", + type: "textfield", + label: "Server URL", + defaultValue: "http://localhost:8337/" + }, + { + id: "username", + type: "textfield", + label: "Username" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + } + ], + + newConfigSaved: function (newConfig) { + Tomahawk.log("Invalidating cache"); + var that = this; + beetsCollection.wipe({id: beetsCollection.settings.id}).then(function () { + window.localStorage.removeItem("beets_trackCount"); + window.localStorage.removeItem("beets_albumCount"); + that.init(); + }); + }, + + _sanitizeConfig: function (config) { + if (!config.server) { + config.server = "http://localhost:8337/"; + } else { + if (config.server.search("^.*:\/\/") < 0) { + // couldn't find a proper protocol, so we default to "http://" + config.server = "http://" + config.server; + } + + // qtwebkit doesn't support toString() or href on URLs + if(URL.prototype.hasOwnProperty('toString')) { + var url = new URL(config.server); + if (!url.port) { + url.port = 8337; + } + config.server = url.toString(); + } + } + + return config; + }, + + init: function () { + var config = this._sanitizeConfig(this.getUserConfig()); + this._server = config.server; + this._username = config.username; + this._password = config.password; + + this._ensureCollection(); + }, + + _ensureCollection: function () { + var that = this; + + return beetsCollection.revision({ + id: beetsCollection.settings.id + }).then(function (result) { + var lastCollectionUpdate = window.localStorage["beets_last_collection_update"]; + if (lastCollectionUpdate && lastCollectionUpdate == result) { + Tomahawk.log("Collection database has not been changed since last time."); + return that._fetchAndStoreCollection(); + } else { + Tomahawk.log("Collection database has been changed. Wiping and re-fetching..."); + window.localStorage.removeItem("beets_trackCount"); + window.localStorage.removeItem("beets_albumCount"); + return beetsCollection.wipe({ + id: beetsCollection.settings.id + }).then(function () { + return that._fetchAndStoreCollection(); + }); + } + }); + }, + + _fetchAndStoreCollection: function () { + var that = this; + + var settings; + if (this._username && this._password) { + settings = { + username: this._username, + password: this._password + }; + } + + Tomahawk.get(this._server + 'stats', settings).then(function (response) { + var trackCount = parseInt(response.items); + var albumCount = parseInt(response.albums); + Tomahawk.PluginManager.registerPlugin("collection", beetsCollection); + if (window.localStorage["beets_trackCount"] != trackCount + || window.localStorage["beets_albumCount"] != albumCount) { + var msg = ""; + if (window.localStorage["beets_trackCount"] != trackCount) { + msg += "Track count has changed from " + window.localStorage["beets_trackCount"] + + " to " + trackCount + ". "; + } + if (window.localStorage["beets_albumCount"] != albumCount) { + msg += "Album count has changed from " + window.localStorage["beets_albumCount"] + + " to " + albumCount + ". "; + } + Tomahawk.log(msg + "Updating collection ..."); + return Tomahawk.get(that._server + "item", settings).then(function (response) { + var searchResults = []; + response.items.forEach(function (item) { + searchResults.push({ + artist: item.artist, + artistDisambiguation: "", + albumArtist: item.artist, + albumArtistDisambiguation: "", + album: item.album, + track: item.title, + albumpos: item.track, + url: that._server + 'item/' + item.id + '/file', + duration: Math.floor(item.length) + }); + }); + beetsCollection.wipe({id: beetsCollection.settings.id}).then(function () { + beetsCollection.addTracks({ + id: beetsCollection.settings.id, + tracks: searchResults + }).then(function (newRevision) { + window.localStorage["beets_trackCount"] = trackCount; + window.localStorage["beets_albumCount"] = albumCount; + window.localStorage["beets_last_collection_update"] = newRevision; + }); + }); + }); + } else { + Tomahawk.log("Track count is still " + trackCount + + ". Album count is still " + albumCount + + ". No collection update necessary."); + beetsCollection.addTracks({ + id: beetsCollection.settings.id, + tracks: [] + }); + } + }); + + }, + + testConfig: function (config) { + config = this._sanitizeConfig(config); + + var settings; + if (config.username && config.password) { + settings = { + username: config.username, + password: config.password + }; + } + + return Tomahawk.get(config.server + "stats", settings).then(function () { + return Tomahawk.ConfigTestResultType.Success; + }, function (xhr) { + if (xhr.status == 403) { + return Tomahawk.ConfigTestResultType.InvalidCredentials; + } else if (xhr.status == 404 || xhr.status == 0) { + return Tomahawk.ConfigTestResultType.CommunicationError; + } else { + return xhr.responseText.trim(); + } + }); + } +}); + +Tomahawk.resolver.instance = BeetsResolver; + +var beetsCollection = Tomahawk.extend(Tomahawk.Collection, { + settings: { + id: "beets", + prettyname: "Beets", + description: BeetsResolver.server, + iconfile: "contents/images/icon.png", + trackcount: BeetsResolver.trackCount + } +}); \ No newline at end of file diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/beets/content/contents/code/config.ui new file mode 100644 index 000000000..1d80d5df9 --- /dev/null +++ b/app/src/main/assets/js/resolvers/beets/content/contents/code/config.ui @@ -0,0 +1,163 @@ + + + Form + + + + 0 + 0 + 290 + 150 + + + + + 290 + 150 + + + + Form + + + + + 10 + 20 + 271 + 111 + + + + + QLayout::SetDefaultConstraint + + + QFormLayout::FieldsStayAtSizeHint + + + + + Server + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + + + http://localhost:8337 + + + + + + + Username + + + + + + + false + + + + 200 + 0 + + + + + + + username + + + + + + + Password + + + + + + + false + + + + 200 + 0 + + + + + + + + + + QLineEdit::Password + + + password + + + + + + + + + + useAuthCheckBox + toggled(bool) + passwordField + setEnabled(bool) + + + 133 + 92 + + + 155 + 152 + + + + + useAuthCheckBox + toggled(bool) + usernameField + setEnabled(bool) + + + 133 + 92 + + + 155 + 121 + + + + + diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/beets/content/contents/images/icon.png new file mode 100644 index 000000000..d662cd690 Binary files /dev/null and b/app/src/main/assets/js/resolvers/beets/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/beets/content/contents/images/iconBackground.png new file mode 100644 index 000000000..f9113a5ea Binary files /dev/null and b/app/src/main/assets/js/resolvers/beets/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/beets/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/beets/content/contents/images/iconWhite.png new file mode 100644 index 000000000..257307c56 Binary files /dev/null and b/app/src/main/assets/js/resolvers/beets/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/beets/content/metadata.json b/app/src/main/assets/js/resolvers/beets/content/metadata.json new file mode 100644 index 000000000..db9d4e959 --- /dev/null +++ b/app/src/main/assets/js/resolvers/beets/content/metadata.json @@ -0,0 +1,24 @@ +{ + "name": "Beets", + "pluginName": "beets", + "author": "Adrian, Uwe, Enno and Dominik", + "email": "uwelk@xhochy.com", + "version": "0.6.3", + "website": "http://gettomahawk.com", + "description": "Connects to a beets server and resolves tracks.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/beets.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/config.ui", + "contents/code/beets-icon.png" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.js b/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.js new file mode 100644 index 000000000..57ee4ab49 --- /dev/null +++ b/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.js @@ -0,0 +1,241 @@ +/* + * Copyright 2013, Uwe L. Korn + * Copyright 2014, Enno Gottschalk + * + * 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. + */ + +var DeezerResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Deezer', + icon: 'deezer.png', + weight: 95, + timeout: 15 + }, + + appId: "138751", + + // Deezer requires the redirectUri to be in the domain that has been defined when + // Tomahawk-Android has been registered on the Deezer Developer website + redirectUri: "tomahawkdeezerresolver://hatchet.is", + + storageKeyAccessToken: "deezer_access_token", + + storageKeyAccessTokenExpires: "deezer_access_token_expires", + + getAccessToken: function () { + var that = this; + + var accessToken = Tomahawk.localStorage.getItem(that.storageKeyAccessToken); + var accessTokenExpires = + Tomahawk.localStorage.getItem(that.storageKeyAccessTokenExpires); + if (accessToken !== null && accessToken.length > 0 && accessTokenExpires !== null) { + return { + accessToken: accessToken, + accessTokenExpires: accessTokenExpires + }; + } else { + throw new Error("There's no accessToken set."); + } + }, + + login: function () { + Tomahawk.log("Starting login"); + + var authUrl = "https://connect.deezer.com/oauth/auth.php"; + authUrl += "?app_id=" + this.appId; + authUrl += "&redirect_uri=" + encodeURIComponent(this.redirectUri); + authUrl += "&perms=offline_access"; + authUrl += "&response_type=token"; + + var that = this; + + var params = { + url: authUrl + }; + return Tomahawk.NativeScriptJobManager.invoke("showWebView", params).then( + function (result) { + var error = that._getParameterByName(result.url, "error_reason"); + if (error) { + Tomahawk.log("Authorization failed: " + error); + return error; + } else { + Tomahawk.log("Authorization successful, received new access token ..."); + that.accessToken = that._getParameterByName(result.url, "access_token"); + that.accessTokenExpires = that._getParameterByName(result.url, "expires"); + Tomahawk.localStorage.setItem(that.storageKeyAccessToken, that.accessToken); + Tomahawk.localStorage.setItem(that.storageKeyAccessTokenExpires, + that.accessTokenExpires); + return TomahawkConfigTestResultType.Success; + } + }); + }, + + logout: function () { + Tomahawk.localStorage.removeItem(this.storageKeyAccessToken); + return TomahawkConfigTestResultType.Logout; + }, + + isLoggedIn: function () { + var accessToken = Tomahawk.localStorage.getItem(this.storageKeyAccessToken); + return accessToken !== null && accessToken.length > 0; + }, + + /** + * Returns the value of the query parameter with the given name from the given URL. + */ + _getParameterByName: function (url, name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&#]" + name + "=([^&#]*)"), results = regex.exec(url); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + }, + + init: function () { + Tomahawk.PluginManager.registerPlugin("linkParser", this); + + this.accessToken = Tomahawk.localStorage.getItem(this.storageKeyAccessToken); + this.accessTokenExpires = Tomahawk.localStorage.getItem(this.storageKeyAccessTokenExpires); + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + var that = this; + + var queryPart; + if (artist) { + queryPart = artist + " " + track; + } else { + queryPart = track; + } + var query = "http://api.deezer.com/search?q=" + encodeURIComponent(queryPart) + + "&limit=100"; + return Tomahawk.get(query).then(function (response) { + var results = []; + for (var i = 0; i < response.data.length; i++) { + var item = response.data[i]; + if (item.type == 'track' && item.readable) { + results.push({ + source: that.settings.name, + artist: item.artist.name, + track: item.title, + duration: item.duration, + url: "deezer://track/" + item.id, + album: item.album.title, + linkUrl: item.link + }); + } + } + return results; + }); + }, + + search: function (params) { + var query = params.query; + + return this.resolve({ + track: query + }); + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + if (!url) { + throw new Error("Provided url was empty or null!"); + } + switch (type) { + case TomahawkUrlType.Album: + return /https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)album\//.test(url); + case TomahawkUrlType.Artist: + return /https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)artist\//.test(url); + case TomahawkUrlType.Playlist: + return /https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)playlist\//.test(url); + case TomahawkUrlType.Track: + return /https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)track\//.test(url); + // case TomahawkUrlType.Any: + default: + return /https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)/.test(url); + } + }, + + lookupUrl: function (params) { + var url = params.url; + Tomahawk.log("lookupUrl: " + url); + + var urlParts = url.split('/').filter(function (item) { + return item.length != 0; + }).map(decodeURIComponent); + + if (/https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)artist\//.test(url)) { + // We have to deal with an artist + var query = 'https://api.deezer.com/2.0/artist/' + urlParts[urlParts.length - 1]; + return Tomahawk.get(query).then(function (response) { + return { + type: Tomahawk.UrlType.Artist, + artist: response.name + }; + }); + } else if (/https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)playlist\//.test(url)) { + // We have to deal with a playlist. + var query = 'https://api.deezer.com/2.0/playlist/' + urlParts[urlParts.length - 1]; + return Tomahawk.get(query).then(function (res) { + var query2 = 'https://api.deezer.com/2.0/playlist/' + res.creator.id; + return Tomahawk.get(query2).then(function (res2) { + return { + type: Tomahawk.UrlType.Playlist, + title: res.title, + guid: "deezer-playlist-" + res.id.toString(), + info: "A playlist by " + res2.name + " on Deezer.", + creator: res2.name, + linkUrl: res.link, + tracks: res.tracks.data.map(function (item) { + return { + type: Tomahawk.UrlType.Track, + track: item.title, + artist: item.artist.name + }; + }) + }; + }); + }); + } else if (/https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)track\//.test(url)) { + // We have to deal with a track. + var query = 'https://api.deezer.com/2.0/track/' + urlParts[urlParts.length - 1]; + return Tomahawk.get(query).then(function (res) { + return { + type: Tomahawk.UrlType.Track, + track: res.title, + artist: res.artist.name + }; + }); + } else if (/https?:\/\/(www\.)?deezer.com\/([^\/]*\/|)album\//.test(url)) { + // We have to deal with an album. + var query = 'https://api.deezer.com/2.0/album/' + urlParts[urlParts.length - 1]; + return Tomahawk.get(query).then(function (res) { + return { + type: Tomahawk.UrlType.Album, + album: res.title, + artist: res.artist.name + }; + }); + } + } +}); + +Tomahawk.resolver.instance = DeezerResolver; + diff --git a/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.png b/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.png new file mode 100644 index 000000000..9ed15cc47 Binary files /dev/null and b/app/src/main/assets/js/resolvers/deezer/content/contents/code/deezer.png differ diff --git a/app/src/main/assets/js/resolvers/deezer/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/deezer/content/contents/images/icon.png new file mode 100644 index 000000000..2feebf08a Binary files /dev/null and b/app/src/main/assets/js/resolvers/deezer/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconBackground.png new file mode 100644 index 000000000..ee1d19eab Binary files /dev/null and b/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconWhite.png new file mode 100644 index 000000000..ef2f01953 Binary files /dev/null and b/app/src/main/assets/js/resolvers/deezer/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/deezer/content/metadata.json b/app/src/main/assets/js/resolvers/deezer/content/metadata.json new file mode 100644 index 000000000..6ffaebf1d --- /dev/null +++ b/app/src/main/assets/js/resolvers/deezer/content/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Deezer", + "pluginName": "deezer", + "author": "Uwe and Enno", + "email": "uwelk@xhochy.com", + "version": "0.2.1", + "website": "http://gettomahawk.com", + "description": "Stream music with Deezer. Premium Deezer account required.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/deezer.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/deezer.png" + ] + } +} diff --git a/app/src/main/assets/js/resolvers/gmusic/.gitignore b/app/src/main/assets/js/resolvers/gmusic/.gitignore new file mode 100644 index 000000000..bfa45d747 --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/.gitignore @@ -0,0 +1,3 @@ + +/makeaxe.rb +/gmusic-* diff --git a/app/src/main/assets/js/resolvers/gmusic/COPYING.txt b/app/src/main/assets/js/resolvers/gmusic/COPYING.txt new file mode 100644 index 000000000..0e259d42c --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/COPYING.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/app/src/main/assets/js/resolvers/gmusic/README.md b/app/src/main/assets/js/resolvers/gmusic/README.md new file mode 100644 index 000000000..e4e05c78b --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/README.md @@ -0,0 +1,15 @@ +Copying +======= + +Written in 2013 by Sam Hanes +Heavily modified in 2014 by Lalit Maganti + +To the extent possible under law, the author(s) have dedicated all +copyright and related and neighboring rights to this software to +the public domain worldwide. This software is distributed without +any warranty. + +You should have received a copy of the CC0 Public Domain Dedication +along with this software. If not, see: +http://creativecommons.org/publicdomain/zero/1.0/ + diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/config.ui new file mode 100644 index 000000000..83e689a5b --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/config.ui @@ -0,0 +1,120 @@ + + + Form + + + + 0 + 0 + 585 + 251 + + + + + 250 + 120 + + + + Form + + + + + + play-logo.png + + + + + + + For this plug-in to work you must first login using the official Google Music iOS or Android app and play a song. After you've done that Tomahawk should then be able to authenticate with your account. + + + true + + + + + + + <html><head/><body><p>Note: If you use 2-Step Verification, then you must create an <a href="https://support.google.com/accounts/answer/185833?hl=en"><span style=" text-decoration: underline; color:#0000ff;">app-specific password</span></a> to use in Tomahawk.</p></body></html> + + + true + + + true + + + + + + + <html><head/><body><p>Otherwise, make sure that you enable "less secure apps" in your <a href="https://www.google.com/settings/security/lesssecureapps"><span style=" text-decoration: underline; color:#0000ff;">Google account settings</span></a>.</p></body></html> + + + true + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Email + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Password + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/code/gmusic.js b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/gmusic.js new file mode 100755 index 000000000..70fb567f9 --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/gmusic.js @@ -0,0 +1,730 @@ +/* Google Play Music resolver for Tomahawk. + * + * Written in 2013 by Sam Hanes + * Extensive modifications in 2014 by Lalit Maganti + * Further modifications in 2014 by Enno Gottschalk + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication + * along with this software. If not, see: + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// We unfortunately need this because Tomahawk-Desktop doesn't properly update metadata.json through +// synchrotron. This should normally be provided in a separate asmcrypto.js file. +// !!!Explicitely endorsed by Domme!!! +!function(a,b){"use strict";function c(){var a=Error.apply(this,arguments);this.message=a.message,this.stack=a.stack}function d(){var a=Error.apply(this,arguments);this.message=a.message,this.stack=a.stack}function e(){var a=Error.apply(this,arguments);this.message=a.message,this.stack=a.stack}function f(a){for(var b=a.length,c=new Uint8Array(b),d=0;b>d;d++){var e=a.charCodeAt(d);if(e>>>8)throw new Error("Wide characters are not allowed");c[d]=e}return c}function g(a){var b,c=[],d=a.length;for(1&d&&(a="0"+a,d++),b=0;d>b;b+=2)c.push(parseInt(a.substr(b,2),16));return new Uint8Array(c)}function h(a){return f(atob(a))}function i(a){for(var b="",c=0;c>>1,a|=a>>>2,a|=a>>>4,a|=a>>>8,a|=a>>>16,a+=1}function m(a){return"number"==typeof a}function n(a){return"string"==typeof a}function o(a){return a instanceof ArrayBuffer}function p(a){return a instanceof Uint8Array}function q(a){return a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array}function r(a,b){var c=b.heap,d=c?c.byteLength:b.heapSize||65536;if(4095&d||0>=d)throw new Error("heap size must be a positive integer and a multiple of 4096");return c=c||new a(new ArrayBuffer(d))}function s(a,b,c,d,e){var f=a.length-b,g=e>f?f:e;return a.set(c.subarray(d,d+g),b),g}function t(a){a=a||{},this.heap=r(Uint8Array,a).subarray(Vb.HEAP_DATA),this.asm=a.asm||Vb(b,null,this.heap.buffer),this.mode=null,this.key=null,this.reset(a)}function u(a){if(void 0!==a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected key type");a=f(a)}var b=a.length;if(16!==b&&24!==b&&32!==b)throw new d("illegal key size");var c=new DataView(a.buffer,a.byteOffset,a.byteLength);this.asm.set_key(b>>2,c.getUint32(0),c.getUint32(4),c.getUint32(8),c.getUint32(12),b>16?c.getUint32(16):0,b>16?c.getUint32(20):0,b>24?c.getUint32(24):0,b>24?c.getUint32(28):0),this.key=a}else if(!this.key)throw new Error("key is required")}function v(a){if(void 0!==a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected iv type");a=f(a)}if(16!==a.length)throw new d("illegal iv size");var b=new DataView(a.buffer,a.byteOffset,a.byteLength);this.iv=a,this.asm.set_iv(b.getUint32(0),b.getUint32(4),b.getUint32(8),b.getUint32(12))}else this.iv=null,this.asm.set_iv(0,0,0,0)}function w(a){this.padding=void 0!==a?!!a:!0}function x(a){return a=a||{},this.result=null,this.pos=0,this.len=0,u.call(this,a.key),this.hasOwnProperty("iv")&&v.call(this,a.iv),this.hasOwnProperty("padding")&&w.call(this,a.padding),this}function y(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");for(var b=this.asm,c=this.heap,d=Vb.ENC[this.mode],e=Vb.HEAP_DATA,g=this.pos,h=this.len,i=0,j=a.length||0,k=0,l=h+j&-16,m=0,q=new Uint8Array(l);j>0;)m=s(c,g+h,a,i,j),h+=m,i+=m,j-=m,m=b.cipher(d,e+g,h),m&&q.set(c.subarray(g,g+m),k),k+=m,h>m?(g+=m,h-=m):(g=0,h=0);return this.result=q,this.pos=g,this.len=h,this}function z(a){var b=null,c=0;void 0!==a&&(b=y.call(this,a).result,c=b.length);var e=this.asm,f=this.heap,g=Vb.ENC[this.mode],h=Vb.HEAP_DATA,i=this.pos,j=this.len,k=16-j%16,l=j;if(this.hasOwnProperty("padding")){if(this.padding){for(var m=0;k>m;++m)f[i+j+m]=k;j+=k,l=j}else if(j%16)throw new d("data length must be a multiple of the block size")}else j+=k;var n=new Uint8Array(c+l);return c&&n.set(b),j&&e.cipher(g,h+i,j),l&&n.set(f.subarray(i,i+l),c),this.result=n,this.pos=0,this.len=0,this}function A(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");var b=this.asm,c=this.heap,d=Vb.DEC[this.mode],e=Vb.HEAP_DATA,g=this.pos,h=this.len,i=0,j=a.length||0,k=0,l=h+j&-16,m=0,q=0;this.hasOwnProperty("padding")&&this.padding&&(m=h+j-l||16,l-=m);for(var r=new Uint8Array(l);j>0;)q=s(c,g+h,a,i,j),h+=q,i+=q,j-=q,q=b.cipher(d,e+g,h-(j?0:m)),q&&r.set(c.subarray(g,g+q),k),k+=q,h>q?(g+=q,h-=q):(g=0,h=0);return this.result=r,this.pos=g,this.len=h,this}function B(a){var b=null,c=0;void 0!==a&&(b=A.call(this,a).result,c=b.length);var f=this.asm,g=this.heap,h=Vb.DEC[this.mode],i=Vb.HEAP_DATA,j=this.pos,k=this.len,l=k;if(k>0){if(k%16){if(this.hasOwnProperty("padding"))throw new d("data length must be a multiple of the block size");k+=16-k%16}if(f.cipher(h,i+j,k),this.hasOwnProperty("padding")&&this.padding){var m=g[j+l-1];if(1>m||m>16||m>l)throw new e("bad padding");for(var n=0,o=m;o>1;o--)n|=m^g[j+l-o];if(n)throw new e("bad padding");l-=m}}var p=new Uint8Array(c+l);return c>0&&p.set(b),l>0&&p.set(g.subarray(j,j+l),c),this.result=p,this.pos=0,this.len=0,this}function C(a){this.padding=!0,this.iv=null,t.call(this,a),this.mode="CBC"}function D(a){C.call(this,a)}function E(a){C.call(this,a)}function F(a){this.nonce=null,this.counter=0,this.counterSize=0,t.call(this,a),this.mode="CTR"}function G(a){F.call(this,a)}function H(a,b,c){if(void 0!==c){if(8>c||c>48)throw new d("illegal counter size");this.counterSize=c;var e=Math.pow(2,c)-1;this.asm.set_mask(0,0,e/4294967296|0,0|e)}else this.counterSize=c=48,this.asm.set_mask(0,0,65535,4294967295);if(void 0===a)throw new Error("nonce is required");if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected nonce type");a=f(a)}var g=a.length;if(!g||g>16)throw new d("illegal nonce size");this.nonce=a;var h=new DataView(new ArrayBuffer(16));if(new Uint8Array(h.buffer).set(a),this.asm.set_nonce(h.getUint32(0),h.getUint32(4),h.getUint32(8),h.getUint32(12)),void 0!==b){if(!m(b))throw new TypeError("unexpected counter type");if(0>b||b>=Math.pow(2,c))throw new d("illegal counter value");this.counter=b,this.asm.set_counter(0,0,b/4294967296|0,0|b)}else this.counter=b=0}function I(a){return a=a||{},x.call(this,a),H.call(this,a.nonce,a.counter,a.counterSize),this}function J(a){for(var b=this.heap,c=this.asm,d=0,e=a.length||0,f=0;e>0;){for(f=s(b,0,a,d,e),d+=f,e-=f;15&f;)b[f++]=0;c.mac(Vb.MAC.GCM,Vb.HEAP_DATA,f)}}function K(a){this.nonce=null,this.adata=null,this.iv=null,this.counter=1,this.tagSize=16,t.call(this,a),this.mode="GCM"}function L(a){K.call(this,a)}function M(a){K.call(this,a)}function N(a){a=a||{},x.call(this,a);var b=this.asm,c=this.heap;b.gcm_init();var e=a.tagSize;if(void 0!==e){if(!m(e))throw new TypeError("tagSize must be a number");if(4>e||e>16)throw new d("illegal tagSize value");this.tagSize=e}else this.tagSize=16;var g=a.nonce;if(void 0===g)throw new Error("nonce is required");if(p(g)||o(g))g=new Uint8Array(g);else{if(!n(g))throw new TypeError("unexpected nonce type");g=f(g)}this.nonce=g;var h=g.length||0,i=new Uint8Array(16);12!==h?(J.call(this,g),c[0]=c[1]=c[2]=c[3]=c[4]=c[5]=c[6]=c[7]=c[8]=c[9]=c[10]=0,c[11]=h>>>29,c[12]=h>>>21&255,c[13]=h>>>13&255,c[14]=h>>>5&255,c[15]=h<<3&255,b.mac(Vb.MAC.GCM,Vb.HEAP_DATA,16),b.get_iv(Vb.HEAP_DATA),b.set_iv(),i.set(c.subarray(0,16))):(i.set(g),i[15]=1);var j=new DataView(i.buffer);this.gamma0=j.getUint32(12),b.set_nonce(j.getUint32(0),j.getUint32(4),j.getUint32(8),0),b.set_mask(0,0,0,4294967295);var k=a.adata;if(void 0!==k&&null!==k){if(p(k)||o(k))k=new Uint8Array(k);else{if(!n(k))throw new TypeError("unexpected adata type");k=f(k)}if(k.length>_b)throw new d("illegal adata length");k.length?(this.adata=k,J.call(this,k)):this.adata=null}else this.adata=null;var l=a.counter;if(void 0!==l){if(!m(l))throw new TypeError("counter must be a number");if(1>l||l>4294967295)throw new RangeError("counter must be a positive 32-bit integer");this.counter=l,b.set_counter(0,0,0,this.gamma0+l|0)}else this.counter=1,b.set_counter(0,0,0,this.gamma0+1|0);var q=a.iv;if(void 0!==q){if(!m(l))throw new TypeError("counter must be a number");this.iv=q,v.call(this,q)}return this}function O(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");var b=0,c=a.length||0,d=this.asm,e=this.heap,g=this.counter,h=this.pos,i=this.len,j=0,k=i+c&-16,l=0;if((g-1<<4)+i+c>_b)throw new RangeError("counter overflow");for(var m=new Uint8Array(k);c>0;)l=s(e,h+i,a,b,c),i+=l,b+=l,c-=l,l=d.cipher(Vb.ENC.CTR,Vb.HEAP_DATA+h,i),l=d.mac(Vb.MAC.GCM,Vb.HEAP_DATA+h,l),l&&m.set(e.subarray(h,h+l),j),g+=l>>>4,j+=l,i>l?(h+=l,i-=l):(h=0,i=0);return this.result=m,this.counter=g,this.pos=h,this.len=i,this}function P(){var a=this.asm,b=this.heap,c=this.counter,d=this.tagSize,e=this.adata,f=this.pos,g=this.len,h=new Uint8Array(g+d);a.cipher(Vb.ENC.CTR,Vb.HEAP_DATA+f,g+15&-16),g&&h.set(b.subarray(f,f+g));for(var i=g;15&i;i++)b[f+i]=0;a.mac(Vb.MAC.GCM,Vb.HEAP_DATA+f,i);var j=null!==e?e.length:0,k=(c-1<<4)+g;return b[0]=b[1]=b[2]=0,b[3]=j>>>29,b[4]=j>>>21,b[5]=j>>>13&255,b[6]=j>>>5&255,b[7]=j<<3&255,b[8]=b[9]=b[10]=0,b[11]=k>>>29,b[12]=k>>>21&255,b[13]=k>>>13&255,b[14]=k>>>5&255,b[15]=k<<3&255,a.mac(Vb.MAC.GCM,Vb.HEAP_DATA,16),a.get_iv(Vb.HEAP_DATA),a.set_counter(0,0,0,this.gamma0),a.cipher(Vb.ENC.CTR,Vb.HEAP_DATA,16),h.set(b.subarray(0,d),g),this.result=h,this.counter=1,this.pos=0,this.len=0,this}function Q(a){var b=O.call(this,a).result,c=P.call(this).result,d=new Uint8Array(b.length+c.length);return b.length&&d.set(b),c.length&&d.set(c,b.length),this.result=d,this}function R(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");var b=0,c=a.length||0,d=this.asm,e=this.heap,g=this.counter,h=this.tagSize,i=this.pos,j=this.len,k=0,l=j+c>h?j+c-h&-16:0,m=j+c-l,q=0;if((g-1<<4)+j+c>_b)throw new RangeError("counter overflow");for(var r=new Uint8Array(l);c>m;)q=s(e,i+j,a,b,c-m),j+=q,b+=q,c-=q,q=d.mac(Vb.MAC.GCM,Vb.HEAP_DATA+i,q),q=d.cipher(Vb.DEC.CTR,Vb.HEAP_DATA+i,q),q&&r.set(e.subarray(i,i+q),k),g+=q>>>4,k+=q,i=0,j=0;return c>0&&(j+=s(e,0,a,b,c)),this.result=r,this.counter=g,this.pos=i,this.len=j,this}function S(){var a=this.asm,b=this.heap,d=this.tagSize,f=this.adata,g=this.counter,h=this.pos,i=this.len,j=i-d,k=0;if(d>i)throw new c("authentication tag not found");for(var l=new Uint8Array(j),m=new Uint8Array(b.subarray(h+j,h+i)),n=j;15&n;n++)b[h+n]=0;k=a.mac(Vb.MAC.GCM,Vb.HEAP_DATA+h,n),k=a.cipher(Vb.DEC.CTR,Vb.HEAP_DATA+h,n),j&&l.set(b.subarray(h,h+j));var o=null!==f?f.length:0,p=(g-1<<4)+i-d;b[0]=b[1]=b[2]=0,b[3]=o>>>29,b[4]=o>>>21,b[5]=o>>>13&255,b[6]=o>>>5&255,b[7]=o<<3&255,b[8]=b[9]=b[10]=0,b[11]=p>>>29,b[12]=p>>>21&255,b[13]=p>>>13&255,b[14]=p>>>5&255,b[15]=p<<3&255,a.mac(Vb.MAC.GCM,Vb.HEAP_DATA,16),a.get_iv(Vb.HEAP_DATA),a.set_counter(0,0,0,this.gamma0),a.cipher(Vb.ENC.CTR,Vb.HEAP_DATA,16);for(var q=0,n=0;d>n;++n)q|=m[n]^b[n];if(q)throw new e("data integrity check failed");return this.result=l,this.counter=1,this.pos=0,this.len=0,this}function T(a){var b=R.call(this,a).result,c=S.call(this).result,d=new Uint8Array(b.length+c.length);return b.length&&d.set(b),c.length&&d.set(c,b.length),this.result=d,this}function U(a,b,c,d){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");return new C({heap:dc,asm:ec,key:b,padding:c,iv:d}).encrypt(a).result}function V(a,b,c,d){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");return new C({heap:dc,asm:ec,key:b,padding:c,iv:d}).decrypt(a).result}function W(a,b,c,d,e){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");if(void 0===c)throw new SyntaxError("nonce required");return new K({heap:dc,asm:ec,key:b,nonce:c,adata:d,tagSize:e}).encrypt(a).result}function X(a,b,c,d,e){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");if(void 0===c)throw new SyntaxError("nonce required");return new K({heap:dc,asm:ec,key:b,nonce:c,adata:d,tagSize:e}).decrypt(a).result}function Y(){return this.result=null,this.pos=0,this.len=0,this.asm.reset(),this}function Z(a){if(null!==this.result)throw new c("state must be reset before processing new data");if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");for(var b=this.asm,d=this.heap,e=this.pos,g=this.len,h=0,i=a.length,j=0;i>0;)j=s(d,e+g,a,h,i),g+=j,h+=j,i-=j,j=b.process(e,g),e+=j,g-=j,g||(e=0);return this.pos=e,this.len=g,this}function $(){if(null!==this.result)throw new c("state must be reset before processing new data");return this.asm.finish(this.pos,this.len,0),this.result=new Uint8Array(this.HASH_SIZE),this.result.set(this.heap.subarray(0,this.HASH_SIZE)),this.pos=0,this.len=0,this}function _(a,b,c){"use asm";var d=0,e=0,f=0,g=0,h=0,i=0;var j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0;var t=new a.Uint8Array(c);function u(G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V){G=G|0;H=H|0;I=I|0;J=J|0;K=K|0;L=L|0;M=M|0;N=N|0;O=O|0;P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;var W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0,Da=0,Ea=0,Fa=0,Ga=0,Ha=0,Ia=0,Ja=0,Ka=0,La=0,Ma=0,Na=0,Oa=0,Pa=0,Qa=0,Ra=0,Sa=0,Ta=0,Ua=0,Va=0,Wa=0,Xa=0,Ya=0,Za=0,$a=0,_a=0,ab=0,bb=0,cb=0,db=0,eb=0,fb=0,gb=0,hb=0,ib=0,jb=0,kb=0;W=d;X=e;Y=f;Z=g;$=h;aa=G+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=H+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=I+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=J+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=K+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=L+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=M+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=N+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=O+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=P+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=Q+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=R+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=S+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=T+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=U+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;aa=V+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=T^O^I^G;ba=_<<1|_>>>31;aa=ba+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=U^P^J^H;ca=_<<1|_>>>31;aa=ca+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=V^Q^K^I;da=_<<1|_>>>31;aa=da+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ba^R^L^J;ea=_<<1|_>>>31;aa=ea+(W<<5|W>>>27)+$+(X&Y|~X&Z)+1518500249|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ca^S^M^K;fa=_<<1|_>>>31;aa=fa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=da^T^N^L;ga=_<<1|_>>>31;aa=ga+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ea^U^O^M;ha=_<<1|_>>>31;aa=ha+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=fa^V^P^N;ia=_<<1|_>>>31;aa=ia+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ga^ba^Q^O;ja=_<<1|_>>>31;aa=ja+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ha^ca^R^P;ka=_<<1|_>>>31;aa=ka+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ia^da^S^Q;la=_<<1|_>>>31;aa=la+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ja^ea^T^R;ma=_<<1|_>>>31;aa=ma+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ka^fa^U^S;na=_<<1|_>>>31;aa=na+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=la^ga^V^T;oa=_<<1|_>>>31;aa=oa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ma^ha^ba^U;pa=_<<1|_>>>31;aa=pa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=na^ia^ca^V;qa=_<<1|_>>>31;aa=qa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=oa^ja^da^ba;ra=_<<1|_>>>31;aa=ra+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=pa^ka^ea^ca;sa=_<<1|_>>>31;aa=sa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=qa^la^fa^da;ta=_<<1|_>>>31;aa=ta+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ra^ma^ga^ea;ua=_<<1|_>>>31;aa=ua+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=sa^na^ha^fa;va=_<<1|_>>>31;aa=va+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ta^oa^ia^ga;wa=_<<1|_>>>31;aa=wa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ua^pa^ja^ha;xa=_<<1|_>>>31;aa=xa+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=va^qa^ka^ia;ya=_<<1|_>>>31;aa=ya+(W<<5|W>>>27)+$+(X^Y^Z)+1859775393|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=wa^ra^la^ja;za=_<<1|_>>>31;aa=za+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=xa^sa^ma^ka;Aa=_<<1|_>>>31;aa=Aa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ya^ta^na^la;Ba=_<<1|_>>>31;aa=Ba+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=za^ua^oa^ma;Ca=_<<1|_>>>31;aa=Ca+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Aa^va^pa^na;Da=_<<1|_>>>31;aa=Da+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ba^wa^qa^oa;Ea=_<<1|_>>>31;aa=Ea+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ca^xa^ra^pa;Fa=_<<1|_>>>31;aa=Fa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Da^ya^sa^qa;Ga=_<<1|_>>>31;aa=Ga+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ea^za^ta^ra;Ha=_<<1|_>>>31;aa=Ha+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Fa^Aa^ua^sa;Ia=_<<1|_>>>31;aa=Ia+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ga^Ba^va^ta;Ja=_<<1|_>>>31;aa=Ja+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ha^Ca^wa^ua;Ka=_<<1|_>>>31;aa=Ka+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ia^Da^xa^va;La=_<<1|_>>>31;aa=La+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ja^Ea^ya^wa;Ma=_<<1|_>>>31;aa=Ma+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ka^Fa^za^xa;Na=_<<1|_>>>31;aa=Na+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=La^Ga^Aa^ya;Oa=_<<1|_>>>31;aa=Oa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ma^Ha^Ba^za;Pa=_<<1|_>>>31;aa=Pa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Na^Ia^Ca^Aa;Qa=_<<1|_>>>31;aa=Qa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Oa^Ja^Da^Ba;Ra=_<<1|_>>>31;aa=Ra+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Pa^Ka^Ea^Ca;Sa=_<<1|_>>>31;aa=Sa+(W<<5|W>>>27)+$+(X&Y|X&Z|Y&Z)-1894007588|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Qa^La^Fa^Da;Ta=_<<1|_>>>31;aa=Ta+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ra^Ma^Ga^Ea;Ua=_<<1|_>>>31;aa=Ua+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Sa^Na^Ha^Fa;Va=_<<1|_>>>31;aa=Va+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ta^Oa^Ia^Ga;Wa=_<<1|_>>>31;aa=Wa+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ua^Pa^Ja^Ha;Xa=_<<1|_>>>31;aa=Xa+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Va^Qa^Ka^Ia;Ya=_<<1|_>>>31;aa=Ya+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Wa^Ra^La^Ja;Za=_<<1|_>>>31;aa=Za+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Xa^Sa^Ma^Ka;$a=_<<1|_>>>31;aa=$a+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Ya^Ta^Na^La;_a=_<<1|_>>>31;aa=_a+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=Za^Ua^Oa^Ma;ab=_<<1|_>>>31;aa=ab+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=$a^Va^Pa^Na;bb=_<<1|_>>>31;aa=bb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=_a^Wa^Qa^Oa;cb=_<<1|_>>>31;aa=cb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=ab^Xa^Ra^Pa;db=_<<1|_>>>31;aa=db+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=bb^Ya^Sa^Qa;eb=_<<1|_>>>31;aa=eb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=cb^Za^Ta^Ra;fb=_<<1|_>>>31;aa=fb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=db^$a^Ua^Sa;gb=_<<1|_>>>31;aa=gb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=eb^_a^Va^Ta;hb=_<<1|_>>>31;aa=hb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=fb^ab^Wa^Ua;ib=_<<1|_>>>31;aa=ib+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=gb^bb^Xa^Va;jb=_<<1|_>>>31;aa=jb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;_=hb^cb^Ya^Wa;kb=_<<1|_>>>31;aa=kb+(W<<5|W>>>27)+$+(X^Y^Z)-899497514|0;$=Z;Z=Y;Y=X<<30|X>>>2;X=W;W=aa;d=d+W|0;e=e+X|0;f=f+Y|0;g=g+Z|0;h=h+$|0}function v(G){G=G|0;u(t[G|0]<<24|t[G|1]<<16|t[G|2]<<8|t[G|3],t[G|4]<<24|t[G|5]<<16|t[G|6]<<8|t[G|7],t[G|8]<<24|t[G|9]<<16|t[G|10]<<8|t[G|11],t[G|12]<<24|t[G|13]<<16|t[G|14]<<8|t[G|15],t[G|16]<<24|t[G|17]<<16|t[G|18]<<8|t[G|19],t[G|20]<<24|t[G|21]<<16|t[G|22]<<8|t[G|23],t[G|24]<<24|t[G|25]<<16|t[G|26]<<8|t[G|27],t[G|28]<<24|t[G|29]<<16|t[G|30]<<8|t[G|31],t[G|32]<<24|t[G|33]<<16|t[G|34]<<8|t[G|35],t[G|36]<<24|t[G|37]<<16|t[G|38]<<8|t[G|39],t[G|40]<<24|t[G|41]<<16|t[G|42]<<8|t[G|43],t[G|44]<<24|t[G|45]<<16|t[G|46]<<8|t[G|47],t[G|48]<<24|t[G|49]<<16|t[G|50]<<8|t[G|51],t[G|52]<<24|t[G|53]<<16|t[G|54]<<8|t[G|55],t[G|56]<<24|t[G|57]<<16|t[G|58]<<8|t[G|59],t[G|60]<<24|t[G|61]<<16|t[G|62]<<8|t[G|63])}function w(G){G=G|0;t[G|0]=d>>>24;t[G|1]=d>>>16&255;t[G|2]=d>>>8&255;t[G|3]=d&255;t[G|4]=e>>>24;t[G|5]=e>>>16&255;t[G|6]=e>>>8&255;t[G|7]=e&255;t[G|8]=f>>>24;t[G|9]=f>>>16&255;t[G|10]=f>>>8&255;t[G|11]=f&255;t[G|12]=g>>>24;t[G|13]=g>>>16&255;t[G|14]=g>>>8&255;t[G|15]=g&255;t[G|16]=h>>>24;t[G|17]=h>>>16&255;t[G|18]=h>>>8&255;t[G|19]=h&255}function x(){d=1732584193;e=4023233417;f=2562383102;g=271733878;h=3285377520;i=0}function y(G,H,I,J,K,L){G=G|0;H=H|0;I=I|0;J=J|0;K=K|0;L=L|0;d=G;e=H;f=I;g=J;h=K;i=L}function z(G,H){G=G|0;H=H|0;var I=0;if(G&63)return-1;while((H|0)>=64){v(G);G=G+64|0;H=H-64|0;I=I+64|0}i=i+I|0;return I|0}function A(G,H,I){G=G|0;H=H|0;I=I|0;var J=0,K=0;if(G&63)return-1;if(~I)if(I&31)return-1;if((H|0)>=64){J=z(G,H)|0;if((J|0)==-1)return-1;G=G+J|0;H=H-J|0}J=J+H|0;i=i+H|0;t[G|H]=128;if((H|0)>=56){for(K=H+1|0;(K|0)<64;K=K+1|0)t[G|K]=0;v(G);H=0;t[G|0]=0}for(K=H+1|0;(K|0)<59;K=K+1|0)t[G|K]=0;t[G|59]=i>>>29;t[G|60]=i>>>21&255;t[G|61]=i>>>13&255;t[G|62]=i>>>5&255;t[G|63]=i<<3&255;v(G);if(~I)w(I);return J|0}function B(){d=j;e=k;f=l;g=m;h=n;i=64}function C(){d=o;e=p;f=q;g=r;h=s;i=64}function D(G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V){G=G|0;H=H|0;I=I|0;J=J|0;K=K|0;L=L|0;M=M|0;N=N|0;O=O|0;P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;x();u(G^1549556828,H^1549556828,I^1549556828,J^1549556828,K^1549556828,L^1549556828,M^1549556828,N^1549556828,O^1549556828,P^1549556828,Q^1549556828,R^1549556828,S^1549556828,T^1549556828,U^1549556828,V^1549556828);o=d;p=e;q=f;r=g;s=h;x();u(G^909522486,H^909522486,I^909522486,J^909522486,K^909522486,L^909522486,M^909522486,N^909522486,O^909522486,P^909522486,Q^909522486,R^909522486,S^909522486,T^909522486,U^909522486,V^909522486);j=d;k=e;l=f;m=g;n=h;i=64}function E(G,H,I){G=G|0;H=H|0;I=I|0;var J=0,K=0,L=0,M=0,N=0,O=0;if(G&63)return-1;if(~I)if(I&31)return-1;O=A(G,H,-1)|0;J=d,K=e,L=f,M=g,N=h;C();u(J,K,L,M,N,2147483648,0,0,0,0,0,0,0,0,0,672);if(~I)w(I);return O|0}function F(G,H,I,J,K){G=G|0;H=H|0;I=I|0;J=J|0;K=K|0;var L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0;if(G&63)return-1;if(~K)if(K&31)return-1;t[G+H|0]=I>>>24;t[G+H+1|0]=I>>>16&255;t[G+H+2|0]=I>>>8&255;t[G+H+3|0]=I&255;E(G,H+4|0,-1)|0;L=Q=d,M=R=e,N=S=f,O=T=g,P=U=h;J=J-1|0;while((J|0)>0){B();u(Q,R,S,T,U,2147483648,0,0,0,0,0,0,0,0,0,672);Q=d,R=e,S=f,T=g,U=h;C();u(Q,R,S,T,U,2147483648,0,0,0,0,0,0,0,0,0,672);Q=d,R=e,S=f,T=g,U=h;L=L^d;M=M^e;N=N^f;O=O^g;P=P^h;J=J-1|0}d=L;e=M;f=N;g=O;h=P;if(~K)w(K);return 0}return{reset:x,init:y,process:z,finish:A,hmac_reset:B,hmac_init:D,hmac_finish:E,pbkdf2_generate_block:F}}function aa(a){a=a||{},this.heap=r(Uint8Array,a),this.asm=a.asm||_(b,null,this.heap.buffer),this.BLOCK_SIZE=fc,this.HASH_SIZE=gc,this.reset()}function ba(){return null===ic&&(ic=new aa({heapSize:1048576})),ic}function ca(a){if(void 0===a)throw new SyntaxError("data required");return ba().reset().process(a).finish().result}function da(a){var b=ca(a);return j(b)}function ea(a){var b=ca(a);return k(b)}function fa(a,b,c){"use asm";var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0;var m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0;var C=new a.Uint8Array(c);function D(P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca){P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;var da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0;da=d;ea=e;fa=f;ga=g;ha=h;ia=i;ja=j;ka=k;la=P+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1116352408|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=Q+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1899447441|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=R+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3049323471|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=S+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3921009573|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=T+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+961987163|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=U+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1508970993|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=V+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2453635748|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=W+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2870763221|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=X+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3624381080|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=Y+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+310598401|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=Z+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+607225278|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=$+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1426881987|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=_+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1925078388|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=aa+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2162078206|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=ba+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2614888103|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;la=ca+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3248222580|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;P=la=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+P+Y|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3835390401|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Q=la=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+4022224774|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;R=la=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(P>>>17^P>>>19^P>>>10^P<<15^P<<13)+R+$|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+264347078|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;S=la=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+604807628|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;T=la=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+770255983|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;U=la=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1249150122|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;V=la=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1555081692|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;W=la=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+P|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1996064986|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;X=la=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2554220882|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Y=la=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2821834349|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Z=la=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2952996808|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;$=la=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3210313671|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;_=la=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3336571891|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;aa=la=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3584528711|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ba=la=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+113926993|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ca=la=(P>>>7^P>>>18^P>>>3^P<<25^P<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+338241895|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;P=la=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+P+Y|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+666307205|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Q=la=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+773529912|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;R=la=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(P>>>17^P>>>19^P>>>10^P<<15^P<<13)+R+$|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1294757372|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;S=la=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1396182291|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;T=la=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1695183700|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;U=la=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1986661051|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;V=la=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2177026350|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;W=la=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+P|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2456956037|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;X=la=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2730485921|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Y=la=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2820302411|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Z=la=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3259730800|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;$=la=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3345764771|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;_=la=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3516065817|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;aa=la=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3600352804|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ba=la=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+4094571909|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ca=la=(P>>>7^P>>>18^P>>>3^P<<25^P<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+275423344|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;P=la=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+P+Y|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+430227734|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Q=la=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+506948616|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;R=la=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(P>>>17^P>>>19^P>>>10^P<<15^P<<13)+R+$|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+659060556|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;S=la=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+883997877|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;T=la=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+958139571|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;U=la=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1322822218|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;V=la=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1537002063|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;W=la=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+P|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1747873779|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;X=la=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+1955562222|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Y=la=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2024104815|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;Z=la=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2227730452|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;$=la=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2361852424|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;_=la=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2428436474|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;aa=la=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+2756734187|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ba=la=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3204031479|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;ca=la=(P>>>7^P>>>18^P>>>3^P<<25^P<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;la=la+ka+(ha>>>6^ha>>>11^ha>>>25^ha<<26^ha<<21^ha<<7)+(ja^ha&(ia^ja))+3329325298|0;ka=ja;ja=ia;ia=ha;ha=ga+la|0;ga=fa;fa=ea;ea=da;da=la+(ea&fa^ga&(ea^fa))+(ea>>>2^ea>>>13^ea>>>22^ea<<30^ea<<19^ea<<10)|0;d=d+da|0;e=e+ea|0;f=f+fa|0;g=g+ga|0;h=h+ha|0;i=i+ia|0;j=j+ja|0;k=k+ka|0}function E(P){P=P|0;D(C[P|0]<<24|C[P|1]<<16|C[P|2]<<8|C[P|3],C[P|4]<<24|C[P|5]<<16|C[P|6]<<8|C[P|7],C[P|8]<<24|C[P|9]<<16|C[P|10]<<8|C[P|11],C[P|12]<<24|C[P|13]<<16|C[P|14]<<8|C[P|15],C[P|16]<<24|C[P|17]<<16|C[P|18]<<8|C[P|19],C[P|20]<<24|C[P|21]<<16|C[P|22]<<8|C[P|23],C[P|24]<<24|C[P|25]<<16|C[P|26]<<8|C[P|27],C[P|28]<<24|C[P|29]<<16|C[P|30]<<8|C[P|31],C[P|32]<<24|C[P|33]<<16|C[P|34]<<8|C[P|35],C[P|36]<<24|C[P|37]<<16|C[P|38]<<8|C[P|39],C[P|40]<<24|C[P|41]<<16|C[P|42]<<8|C[P|43],C[P|44]<<24|C[P|45]<<16|C[P|46]<<8|C[P|47],C[P|48]<<24|C[P|49]<<16|C[P|50]<<8|C[P|51],C[P|52]<<24|C[P|53]<<16|C[P|54]<<8|C[P|55],C[P|56]<<24|C[P|57]<<16|C[P|58]<<8|C[P|59],C[P|60]<<24|C[P|61]<<16|C[P|62]<<8|C[P|63])}function F(P){P=P|0;C[P|0]=d>>>24;C[P|1]=d>>>16&255;C[P|2]=d>>>8&255;C[P|3]=d&255;C[P|4]=e>>>24;C[P|5]=e>>>16&255;C[P|6]=e>>>8&255;C[P|7]=e&255;C[P|8]=f>>>24;C[P|9]=f>>>16&255;C[P|10]=f>>>8&255;C[P|11]=f&255;C[P|12]=g>>>24;C[P|13]=g>>>16&255;C[P|14]=g>>>8&255;C[P|15]=g&255;C[P|16]=h>>>24;C[P|17]=h>>>16&255;C[P|18]=h>>>8&255;C[P|19]=h&255;C[P|20]=i>>>24;C[P|21]=i>>>16&255;C[P|22]=i>>>8&255;C[P|23]=i&255;C[P|24]=j>>>24;C[P|25]=j>>>16&255;C[P|26]=j>>>8&255;C[P|27]=j&255;C[P|28]=k>>>24;C[P|29]=k>>>16&255;C[P|30]=k>>>8&255;C[P|31]=k&255}function G(){d=1779033703;e=3144134277;f=1013904242;g=2773480762;h=1359893119;i=2600822924;j=528734635;k=1541459225;l=0}function H(P,Q,R,S,T,U,V,W,X){P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;d=P;e=Q;f=R;g=S;h=T;i=U;j=V;k=W;l=X}function I(P,Q){P=P|0;Q=Q|0;var R=0;if(P&63)return-1;while((Q|0)>=64){E(P);P=P+64|0;Q=Q-64|0;R=R+64|0}l=l+R|0;return R|0}function J(P,Q,R){P=P|0;Q=Q|0;R=R|0;var S=0,T=0;if(P&63)return-1;if(~R)if(R&31)return-1;if((Q|0)>=64){S=I(P,Q)|0;if((S|0)==-1)return-1;P=P+S|0;Q=Q-S|0}S=S+Q|0;l=l+Q|0;C[P|Q]=128;if((Q|0)>=56){for(T=Q+1|0;(T|0)<64;T=T+1|0)C[P|T]=0;E(P);Q=0;C[P|0]=0}for(T=Q+1|0;(T|0)<59;T=T+1|0)C[P|T]=0;C[P|59]=l>>>29;C[P|60]=l>>>21&255;C[P|61]=l>>>13&255;C[P|62]=l>>>5&255;C[P|63]=l<<3&255;E(P);if(~R)F(R);return S|0}function K(){d=m;e=n;f=o;g=p;h=q;i=r;j=s;k=t;l=64}function L(){d=u;e=v;f=w;g=x;h=y;i=z;j=A;k=B;l=64}function M(P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca){P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;G();D(P^1549556828,Q^1549556828,R^1549556828,S^1549556828,T^1549556828,U^1549556828,V^1549556828,W^1549556828,X^1549556828,Y^1549556828,Z^1549556828,$^1549556828,_^1549556828,aa^1549556828,ba^1549556828,ca^1549556828);u=d;v=e;w=f;x=g;y=h;z=i;A=j;B=k;G();D(P^909522486,Q^909522486,R^909522486,S^909522486,T^909522486,U^909522486,V^909522486,W^909522486,X^909522486,Y^909522486,Z^909522486,$^909522486,_^909522486,aa^909522486,ba^909522486,ca^909522486);m=d;n=e;o=f;p=g;q=h;r=i;s=j;t=k;l=64}function N(P,Q,R){P=P|0;Q=Q|0;R=R|0;var S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0;if(P&63)return-1;if(~R)if(R&31)return-1;$=J(P,Q,-1)|0;S=d,T=e,U=f,V=g,W=h,X=i,Y=j,Z=k;L();D(S,T,U,V,W,X,Y,Z,2147483648,0,0,0,0,0,0,768);if(~R)F(R);return $|0}function O(P,Q,R,S,T){P=P|0;Q=Q|0;R=R|0;S=S|0;T=T|0;var U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0;if(P&63)return-1;if(~T)if(T&31)return-1;C[P+Q|0]=R>>>24;C[P+Q+1|0]=R>>>16&255;C[P+Q+2|0]=R>>>8&255;C[P+Q+3|0]=R&255;N(P,Q+4|0,-1)|0;U=aa=d,V=ba=e,W=ca=f,X=da=g,Y=ea=h,Z=fa=i,$=ga=j,_=ha=k;S=S-1|0;while((S|0)>0){K();D(aa,ba,ca,da,ea,fa,ga,ha,2147483648,0,0,0,0,0,0,768);aa=d,ba=e,ca=f,da=g,ea=h,fa=i,ga=j,ha=k;L();D(aa,ba,ca,da,ea,fa,ga,ha,2147483648,0,0,0,0,0,0,768);aa=d,ba=e,ca=f,da=g,ea=h,fa=i,ga=j,ha=k;U=U^d;V=V^e;W=W^f;X=X^g;Y=Y^h;Z=Z^i;$=$^j;_=_^k;S=S-1|0}d=U;e=V;f=W;g=X;h=Y;i=Z;j=$;k=_;if(~T)F(T);return 0}return{reset:G,init:H,process:I,finish:J,hmac_reset:K,hmac_init:M,hmac_finish:N,pbkdf2_generate_block:O}}function ga(a){a=a||{},this.heap=r(Uint8Array,a),this.asm=a.asm||fa(b,null,this.heap.buffer),this.BLOCK_SIZE=jc,this.HASH_SIZE=kc,this.reset()}function ha(){return null===mc&&(mc=new ga({heapSize:1048576})),mc}function ia(a){if(void 0===a)throw new SyntaxError("data required");return ha().reset().process(a).finish().result}function ja(a){var b=ia(a);return j(b)}function ka(a){var b=ia(a);return k(b)}function la(a){if(a=a||{},!a.hash)throw new SyntaxError("option 'hash' is required");if(!a.hash.HASH_SIZE)throw new SyntaxError("option 'hash' supplied doesn't seem to be a valid hash function");return this.hash=a.hash,this.BLOCK_SIZE=this.hash.BLOCK_SIZE,this.HMAC_SIZE=this.hash.HASH_SIZE,this.key=null,this.verify=null,this.result=null,(void 0!==a.password||void 0!==a.verify)&&this.reset(a),this}function ma(a,b){if(o(b)&&(b=new Uint8Array(b)),n(b)&&(b=f(b)),!p(b))throw new TypeError("password isn't of expected type");var c=new Uint8Array(a.BLOCK_SIZE);return c.set(b.length>a.BLOCK_SIZE?a.reset().process(b).finish().result:b),c}function na(a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("verify tag isn't of expected type");a=f(a)}if(a.length!==this.HMAC_SIZE)throw new d("illegal verification tag size");this.verify=a}function oa(a){a=a||{};var b=a.password;if(null===this.key&&!n(b)&&!b)throw new c("no key is associated with the instance");this.result=null,this.hash.reset(),(b||n(b))&&(this.key=ma(this.hash,b));for(var d=new Uint8Array(this.key),e=0;e=g;++g){var h=(g-1)*this.hmac.HMAC_SIZE,i=(f>g?0:e%this.hmac.HMAC_SIZE)||this.hmac.HMAC_SIZE,j=new Uint8Array(this.hmac.reset().process(a).process(new Uint8Array([g>>>24&255,g>>>16&255,g>>>8&255,255&g])).finish().result);this.result.set(j.subarray(0,i),h);for(var k=1;b>k;++k){j=new Uint8Array(this.hmac.reset().process(j).finish().result);for(var l=0;i>l;++l)this.result[h+l]^=j[l]}}return this}function Ia(a){return a=a||{},a.hmac instanceof ra||(a.hmac=ua()),Fa.call(this,a),this}function Ja(a,b,e){if(null!==this.result)throw new c("state must be reset before processing new data");if(!a&&!n(a))throw new d("bad 'salt' value");b=b||this.count,e=e||this.length,this.result=new Uint8Array(e);for(var f=Math.ceil(e/this.hmac.HMAC_SIZE),g=1;f>=g;++g){var h=(g-1)*this.hmac.HMAC_SIZE,i=(f>g?0:e%this.hmac.HMAC_SIZE)||this.hmac.HMAC_SIZE;this.hmac.reset().process(a),this.hmac.hash.asm.pbkdf2_generate_block(this.hmac.hash.pos,this.hmac.hash.len,g,b,0),this.result.set(this.hmac.hash.heap.subarray(0,i),h)}return this}function Ka(){return null===uc&&(uc=new Ia),uc}function La(a){return a=a||{},a.hmac instanceof va||(a.hmac=ya()),Fa.call(this,a),this}function Ma(a,b,e){if(null!==this.result)throw new c("state must be reset before processing new data");if(!a&&!n(a))throw new d("bad 'salt' value");b=b||this.count,e=e||this.length,this.result=new Uint8Array(e);for(var f=Math.ceil(e/this.hmac.HMAC_SIZE),g=1;f>=g;++g){var h=(g-1)*this.hmac.HMAC_SIZE,i=(f>g?0:e%this.hmac.HMAC_SIZE)||this.hmac.HMAC_SIZE;this.hmac.reset().process(a),this.hmac.hash.asm.pbkdf2_generate_block(this.hmac.hash.pos,this.hmac.hash.len,g,b,0),this.result.set(this.hmac.hash.heap.subarray(0,i),h)}return this}function Na(){return null===wc&&(wc=new La),wc}function Oa(a,b,c,d){if(void 0===a)throw new SyntaxError("password required");if(void 0===b)throw new SyntaxError("salt required");return Ka().reset({password:a}).generate(b,c,d).result}function Pa(a,b,c,d){var e=Oa(a,b,c,d);return j(e)}function Qa(a,b,c,d){var e=Oa(a,b,c,d);return k(e)}function Ra(a,b,c,d){if(void 0===a)throw new SyntaxError("password required");if(void 0===b)throw new SyntaxError("salt required");return Na().reset({password:a}).generate(b,c,d).result}function Sa(a,b,c,d){var e=Ra(a,b,c,d);return j(e)}function Ta(a,b,c,d){var e=Ra(a,b,c,d);return k(e)}function Ua(){if(void 0!==Dc)d=new Uint8Array(32),xc.call(Dc,d),Gc(d);else{var a,c,d=new Ub(3);d[0]=Bc(),d[1]=Ac(),d[2]=Ec(),d=new Uint8Array(d.buffer);var e=Na();for(a=0;100>a;a++)d=e.reset({password:d}).generate(b.location.href,1e3,32).result,c=Ec(),d[0]^=c>>>24,d[1]^=c>>>16,d[2]^=c>>>8,d[3]^=c;Gc(d)}Hc=0,Ic=!0}function Va(a){if(!o(a)&&!q(a))throw new TypeError("bad seed type");var b=a.byteOffest||0,c=a.byteLength||a.length,d=new Uint8Array(a.buffer||a,b,c);Gc(d),Hc=0;for(var e=0,f=0;f=Lc}function Wa(a){if(Ic||Ua(),!Jc&&void 0===Dc){if(!Mc)throw new e("No strong PRNGs available. Use asmCrypto.random.seed().");void 0!==zc&&zc.error("No strong PRNGs available; your security is greatly lowered. Use asmCrypto.random.seed().")}if(!Nc&&!Jc&&void 0!==Dc&&void 0!==zc){var b=(new Error).stack;Oc[b]|=0,Oc[b]++||zc.warn("asmCrypto PRNG not seeded; your security relies on your system PRNG. If this is not acceptable, use asmCrypto.random.seed().")}if(!o(a)&&!q(a))throw new TypeError("unexpected buffer type");var c,d,f=a.byteOffset||0,g=a.byteLength||a.length,h=new Uint8Array(a.buffer||a,f,g);for(void 0!==Dc&&xc.call(Dc,h),c=0;g>c;c++)0===(3&c)&&(Hc>=1099511627776&&Ua(),d=Fc(),Hc++),h[c]^=d,d>>>=8;return a}function Xa(){(!Ic||Hc>=1099511627776)&&Ua();var a=(1048576*Fc()+(Fc()>>>12))/4503599627370496;return Hc+=2,a}function Ya(a,b){return a*b|0}function Za(a,b,c){"use asm";var d=0;var e=new a.Uint32Array(c);var f=a.Math.imul;function g(u){u=u|0;d=u=u+31&-32;return u|0}function h(u){u=u|0;var v=0;v=d;d=v+(u+31&-32)|0;return v|0}function i(u){u=u|0;d=d-(u+31&-32)|0}function j(u,v,w){u=u|0;v=v|0;w=w|0;var x=0;if((v|0)>(w|0)){for(;(x|0)<(u|0);x=x+4|0){e[w+x>>2]=e[v+x>>2]}}else{for(x=u-4|0;(x|0)>=0;x=x-4|0){e[w+x>>2]=e[v+x>>2]}}}function k(u,v,w){u=u|0;v=v|0;w=w|0;var x=0;for(;(x|0)<(u|0);x=x+4|0){e[w+x>>2]=v}}function l(u,v,w,x){u=u|0;v=v|0;w=w|0;x=x|0;var y=0,z=0,A=0,B=0,C=0;if((x|0)<=0)x=v;if((x|0)<(v|0))v=x;z=1;for(;(C|0)<(v|0);C=C+4|0){y=~e[u+C>>2];A=(y&65535)+z|0;B=(y>>>16)+(A>>>16)|0;e[w+C>>2]=B<<16|A&65535;z=B>>>16}for(;(C|0)<(x|0);C=C+4|0){e[w+C>>2]=z-1|0}return z|0}function m(u,v,w,x){u=u|0;v=v|0;w=w|0;x=x|0;var y=0,z=0,A=0;if((v|0)>(x|0)){for(A=v-4|0;(A|0)>=(x|0);A=A-4|0){if(e[u+A>>2]|0)return 1}}else{for(A=x-4|0;(A|0)>=(v|0);A=A-4|0){if(e[w+A>>2]|0)return-1}}for(;(A|0)>=0;A=A-4|0){y=e[u+A>>2]|0,z=e[w+A>>2]|0;if(y>>>0>>0)return-1;if(y>>>0>z>>>0)return 1}return 0}function n(u,v){u=u|0;v=v|0;var w=0;for(w=v-4|0;(w|0)>=0;w=w-4|0){if(e[u+w>>2]|0)return w+4|0}return 0}function o(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0;if((v|0)<(x|0)){D=u,u=w,w=D;D=v,v=x,x=D}if((z|0)<=0)z=v+4|0;if((z|0)<(x|0))v=x=z;for(;(F|0)<(x|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)+(B&65535)|0)+C|0;E=((A>>>16)+(B>>>16)|0)+(D>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>>16}for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;D=(A&65535)+C|0;E=(A>>>16)+(D>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>>16}for(;(F|0)<(z|0);F=F+4|0){e[y+F>>2]=C|0;C=0}return C|0}function p(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0;if((z|0)<=0)z=(v|0)>(x|0)?v+4|0:x+4|0;if((z|0)<(v|0))v=z;if((z|0)<(x|0))x=z;if((v|0)<(x|0)){for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)-(B&65535)|0)+C|0;E=((A>>>16)-(B>>>16)|0)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}for(;(F|0)<(x|0);F=F+4|0){B=e[w+F>>2]|0;D=C-(B&65535)|0;E=(D>>16)-(B>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}}else{for(;(F|0)<(x|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)-(B&65535)|0)+C|0;E=((A>>>16)-(B>>>16)|0)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;D=(A&65535)+C|0;E=(A>>>16)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}}for(;(F|0)<(z|0);F=F+4|0){e[y+F>>2]=C|0}return C|0}function q(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0;if((v|0)>(x|0)){ua=u,va=v;u=w,v=x;w=ua,x=va}xa=v+x|0;if((z|0)>(xa|0)|(z|0)<=0)z=xa;if((z|0)<(v|0))v=z;if((z|0)<(x|0))x=z;for(;(ya|0)<(v|0);ya=ya+32|0){za=u+ya|0;I=e[(za|0)>>2]|0,J=e[(za|4)>>2]|0,K=e[(za|8)>>2]|0,L=e[(za|12)>>2]|0,M=e[(za|16)>>2]|0,N=e[(za|20)>>2]|0,O=e[(za|24)>>2]|0,P=e[(za|28)>>2]|0,A=I&65535,B=J&65535,C=K&65535,D=L&65535,E=M&65535,F=N&65535,G=O&65535,H=P&65535,I=I>>>16,J=J>>>16,K=K>>>16,L=L>>>16,M=M>>>16,N=N>>>16,O=O>>>16,P=P>>>16;ma=na=oa=pa=qa=ra=sa=ta=0;for(Aa=0;(Aa|0)<(x|0);Aa=Aa+32|0){Ba=w+Aa|0;Ca=y+(ya+Aa|0)|0;Y=e[(Ba|0)>>2]|0,Z=e[(Ba|4)>>2]|0,$=e[(Ba|8)>>2]|0,_=e[(Ba|12)>>2]|0,aa=e[(Ba|16)>>2]|0,ba=e[(Ba|20)>>2]|0,ca=e[(Ba|24)>>2]|0,da=e[(Ba|28)>>2]|0,Q=Y&65535,R=Z&65535,S=$&65535,T=_&65535,U=aa&65535,V=ba&65535,W=ca&65535,X=da&65535,Y=Y>>>16,Z=Z>>>16,$=$>>>16,_=_>>>16,aa=aa>>>16,ba=ba>>>16,ca=ca>>>16,da=da>>>16;ea=e[(Ca|0)>>2]|0,fa=e[(Ca|4)>>2]|0,ga=e[(Ca|8)>>2]|0,ha=e[(Ca|12)>>2]|0,ia=e[(Ca|16)>>2]|0,ja=e[(Ca|20)>>2]|0,ka=e[(Ca|24)>>2]|0,la=e[(Ca|28)>>2]|0;ua=((f(A,Q)|0)+(ma&65535)|0)+(ea&65535)|0;va=((f(I,Q)|0)+(ma>>>16)|0)+(ea>>>16)|0;wa=((f(A,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ea=wa<<16|ua&65535;ua=((f(A,R)|0)+(xa&65535)|0)+(fa&65535)|0;va=((f(I,R)|0)+(xa>>>16)|0)+(fa>>>16)|0;wa=((f(A,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;fa=wa<<16|ua&65535;ua=((f(A,S)|0)+(xa&65535)|0)+(ga&65535)|0;va=((f(I,S)|0)+(xa>>>16)|0)+(ga>>>16)|0;wa=((f(A,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(A,T)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(I,T)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(A,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(A,U)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(I,U)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(A,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(A,V)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(I,V)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(A,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(A,W)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(I,W)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(A,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(A,X)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(I,X)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(A,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,da)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ma=xa;ua=((f(B,Q)|0)+(na&65535)|0)+(fa&65535)|0;va=((f(J,Q)|0)+(na>>>16)|0)+(fa>>>16)|0;wa=((f(B,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;fa=wa<<16|ua&65535;ua=((f(B,R)|0)+(xa&65535)|0)+(ga&65535)|0;va=((f(J,R)|0)+(xa>>>16)|0)+(ga>>>16)|0;wa=((f(B,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(B,S)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(J,S)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(B,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(B,T)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(J,T)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(B,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(B,U)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(J,U)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(B,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(B,V)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(J,V)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(B,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(B,W)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(J,W)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(B,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(B,X)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(J,X)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(B,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,da)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;na=xa;ua=((f(C,Q)|0)+(oa&65535)|0)+(ga&65535)|0;va=((f(K,Q)|0)+(oa>>>16)|0)+(ga>>>16)|0;wa=((f(C,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(C,R)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(K,R)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(C,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(C,S)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(K,S)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(C,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(C,T)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(K,T)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(C,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(C,U)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(K,U)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(C,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(C,V)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(K,V)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(C,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(C,W)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(K,W)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(C,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(C,X)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(K,X)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(C,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,da)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;oa=xa;ua=((f(D,Q)|0)+(pa&65535)|0)+(ha&65535)|0;va=((f(L,Q)|0)+(pa>>>16)|0)+(ha>>>16)|0;wa=((f(D,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(D,R)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(L,R)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(D,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(D,S)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(L,S)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(D,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(D,T)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(L,T)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(D,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(D,U)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(L,U)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(D,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(D,V)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(L,V)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(D,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(D,W)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(L,W)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(D,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(D,X)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(L,X)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(D,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,da)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;pa=xa;ua=((f(E,Q)|0)+(qa&65535)|0)+(ia&65535)|0;va=((f(M,Q)|0)+(qa>>>16)|0)+(ia>>>16)|0;wa=((f(E,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(E,R)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(M,R)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(E,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(E,S)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(M,S)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(E,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(E,T)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(M,T)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(E,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,_)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(E,U)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(M,U)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(E,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(E,V)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(M,V)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(E,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(E,W)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(M,W)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(E,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(E,X)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(M,X)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(E,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,da)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;qa=xa;ua=((f(F,Q)|0)+(ra&65535)|0)+(ja&65535)|0;va=((f(N,Q)|0)+(ra>>>16)|0)+(ja>>>16)|0;wa=((f(F,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(F,R)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(N,R)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(F,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(F,S)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(N,S)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(F,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,$)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(F,T)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(N,T)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(F,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(F,U)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(N,U)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(F,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(F,V)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(N,V)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(F,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(F,W)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(N,W)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(F,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(F,X)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(N,X)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(F,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,da)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ra=xa;ua=((f(G,Q)|0)+(sa&65535)|0)+(ka&65535)|0;va=((f(O,Q)|0)+(sa>>>16)|0)+(ka>>>16)|0;wa=((f(G,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(G,R)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(O,R)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(G,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(G,S)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(O,S)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(G,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(G,T)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(O,T)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(G,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,_)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(G,U)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(O,U)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(G,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(G,V)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(O,V)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(G,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(G,W)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(O,W)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(G,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ua=((f(G,X)|0)+(xa&65535)|0)+(ra&65535)|0;va=((f(O,X)|0)+(xa>>>16)|0)+(ra>>>16)|0;wa=((f(G,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,da)|0)+(va>>>16)|0)+(wa>>>16)|0;ra=wa<<16|ua&65535;sa=xa;ua=((f(H,Q)|0)+(ta&65535)|0)+(la&65535)|0;va=((f(P,Q)|0)+(ta>>>16)|0)+(la>>>16)|0;wa=((f(H,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(H,R)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(P,R)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(H,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(H,S)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(P,S)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(H,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,$)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(H,T)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(P,T)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(H,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,_)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(H,U)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(P,U)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(H,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(H,V)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(P,V)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(H,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ua=((f(H,W)|0)+(xa&65535)|0)+(ra&65535)|0;va=((f(P,W)|0)+(xa>>>16)|0)+(ra>>>16)|0;wa=((f(H,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ra=wa<<16|ua&65535;ua=((f(H,X)|0)+(xa&65535)|0)+(sa&65535)|0;va=((f(P,X)|0)+(xa>>>16)|0)+(sa>>>16)|0;wa=((f(H,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,da)|0)+(va>>>16)|0)+(wa>>>16)|0;sa=wa<<16|ua&65535;ta=xa;e[(Ca|0)>>2]=ea,e[(Ca|4)>>2]=fa,e[(Ca|8)>>2]=ga,e[(Ca|12)>>2]=ha,e[(Ca|16)>>2]=ia,e[(Ca|20)>>2]=ja,e[(Ca|24)>>2]=ka,e[(Ca|28)>>2]=la}Ca=y+(ya+Aa|0)|0;e[(Ca|0)>>2]=ma,e[(Ca|4)>>2]=na,e[(Ca|8)>>2]=oa,e[(Ca|12)>>2]=pa,e[(Ca|16)>>2]=qa,e[(Ca|20)>>2]=ra,e[(Ca|24)>>2]=sa,e[(Ca|28)>>2]=ta}}function r(u,v,w){u=u|0;v=v|0;w=w|0;var x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0,Da=0,Ea=0,Fa=0,Ga=0;for(;(Ba|0)<(v|0);Ba=Ba+4|0){Ga=w+(Ba<<1)|0;F=e[u+Ba>>2]|0,x=F&65535,F=F>>>16;ra=f(x,x)|0;sa=(f(x,F)|0)+(ra>>>17)|0;ta=(f(F,F)|0)+(sa>>>15)|0;e[Ga>>2]=sa<<17|ra&131071;e[(Ga|4)>>2]=ta}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+8|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16;V=e[(Ea|4)>>2]|0,N=V&65535,V=V>>>16;ra=f(x,N)|0;sa=(f(x,V)|0)+(ra>>>16)|0;ta=(f(F,N)|0)+(sa&65535)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;xa=e[(Ga|4)>>2]|0;ra=(xa&65535)+((ra&65535)<<1)|0;ta=((xa>>>16)+((ta&65535)<<1)|0)+(ra>>>16)|0;e[(Ga|4)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|8)>>2]|0;ra=((xa&65535)+((wa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(wa>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|8)>>2]=ta<<16|ra&65535;ua=ta>>>16;if(ua){xa=e[(Ga|12)>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[(Ga|12)>>2]=ta<<16|ra&65535}}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+16|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16;V=e[(Ea|8)>>2]|0,N=V&65535,V=V>>>16,W=e[(Ea|12)>>2]|0,O=W&65535,W=W>>>16;ra=f(x,N)|0;sa=f(F,N)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=(f(x,O)|0)+(wa&65535)|0;sa=(f(F,O)|0)+(wa>>>16)|0;ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;da=wa;ra=(f(y,N)|0)+(ca&65535)|0;sa=(f(G,N)|0)+(ca>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ea=wa;xa=e[(Ga|8)>>2]|0;ra=(xa&65535)+((ba&65535)<<1)|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|8)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|12)>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|12)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|16)>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|16)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|20)>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|20)>>2]=ta<<16|ra&65535;ua=ta>>>16;for(Da=24;!!ua&(Da|0)<32;Da=Da+4|0){xa=e[(Ga|Da)>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[(Ga|Da)>>2]=ta<<16|ra&65535;ua=ta>>>16}}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+32|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16,H=e[(Ea|8)>>2]|0,z=H&65535,H=H>>>16,I=e[(Ea|12)>>2]|0,A=I&65535,I=I>>>16;V=e[(Ea|16)>>2]|0,N=V&65535,V=V>>>16,W=e[(Ea|20)>>2]|0,O=W&65535,W=W>>>16,X=e[(Ea|24)>>2]|0,P=X&65535,X=X>>>16,Y=e[(Ea|28)>>2]|0,Q=Y&65535,Y=Y>>>16;ra=f(x,N)|0;sa=f(F,N)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=(f(x,O)|0)+(wa&65535)|0;sa=(f(F,O)|0)+(wa>>>16)|0;ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=(f(x,P)|0)+(wa&65535)|0;sa=(f(F,P)|0)+(wa>>>16)|0;ta=((f(x,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=(f(x,Q)|0)+(wa&65535)|0;sa=(f(F,Q)|0)+(wa>>>16)|0;ta=((f(x,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;fa=wa;ra=(f(y,N)|0)+(ca&65535)|0;sa=(f(G,N)|0)+(ca>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(y,P)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(G,P)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(y,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(y,Q)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(G,Q)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(y,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ga=wa;ra=(f(z,N)|0)+(da&65535)|0;sa=(f(H,N)|0)+(da>>>16)|0;ta=((f(z,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(z,O)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(H,O)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(z,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(z,P)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(H,P)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(z,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(z,Q)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(H,Q)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(z,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ha=wa;ra=(f(A,N)|0)+(ea&65535)|0;sa=(f(I,N)|0)+(ea>>>16)|0;ta=((f(A,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(A,O)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(I,O)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(A,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(A,P)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(I,P)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(A,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(A,Q)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(I,Q)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(A,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ia=wa;xa=e[(Ga|16)>>2]|0;ra=(xa&65535)+((ba&65535)<<1)|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|16)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|20)>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|20)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|24)>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|24)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|28)>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|28)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+32>>2]|0;ra=((xa&65535)+((fa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(fa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+32>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+36>>2]|0;ra=((xa&65535)+((ga&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ga>>>16<<1)|0)+(ra>>>16)|0;e[Ga+36>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+40>>2]|0;ra=((xa&65535)+((ha&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ha>>>16<<1)|0)+(ra>>>16)|0;e[Ga+40>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+44>>2]|0;ra=((xa&65535)+((ia&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ia>>>16<<1)|0)+(ra>>>16)|0;e[Ga+44>>2]=ta<<16|ra&65535;ua=ta>>>16;for(Da=48;!!ua&(Da|0)<64;Da=Da+4|0){xa=e[Ga+Da>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16}}for(ya=32;(ya|0)<(v|0);ya=ya<<1){za=ya<<1;for(Aa=0;(Aa|0)<(v|0);Aa=Aa+za|0){Ga=w+(Aa<<1)|0;va=0;for(Ba=0;(Ba|0)<(ya|0);Ba=Ba+32|0){Ea=(u+Aa|0)+Ba|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16,H=e[(Ea|8)>>2]|0,z=H&65535,H=H>>>16,I=e[(Ea|12)>>2]|0,A=I&65535,I=I>>>16,J=e[(Ea|16)>>2]|0,B=J&65535,J=J>>>16,K=e[(Ea|20)>>2]|0,C=K&65535,K=K>>>16,L=e[(Ea|24)>>2]|0,D=L&65535,L=L>>>16,M=e[(Ea|28)>>2]|0,E=M&65535,M=M>>>16;ja=ka=la=ma=na=oa=pa=qa=ua=0;for(Ca=0;(Ca|0)<(ya|0);Ca=Ca+32|0){Fa=((u+Aa|0)+ya|0)+Ca|0;V=e[Fa>>2]|0,N=V&65535,V=V>>>16,W=e[(Fa|4)>>2]|0,O=W&65535,W=W>>>16,X=e[(Fa|8)>>2]|0,P=X&65535,X=X>>>16,Y=e[(Fa|12)>>2]|0,Q=Y&65535,Y=Y>>>16,Z=e[(Fa|16)>>2]|0,R=Z&65535,Z=Z>>>16,$=e[(Fa|20)>>2]|0,S=$&65535,$=$>>>16,_=e[(Fa|24)>>2]|0,T=_&65535,_=_>>>16,aa=e[(Fa|28)>>2]|0,U=aa&65535,aa=aa>>>16;ba=ca=da=ea=fa=ga=ha=ia=0;ra=((f(x,N)|0)+(ba&65535)|0)+(ja&65535)|0;sa=((f(F,N)|0)+(ba>>>16)|0)+(ja>>>16)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=((f(x,O)|0)+(ca&65535)|0)+(wa&65535)|0;sa=((f(F,O)|0)+(ca>>>16)|0)+(wa>>>16)|0;ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(x,P)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(F,P)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(x,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(x,Q)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(F,Q)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(x,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(x,R)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(F,R)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(x,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(x,S)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(F,S)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(x,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(x,T)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(F,T)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(x,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(x,U)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(F,U)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(x,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ja=wa;ra=((f(y,N)|0)+(ca&65535)|0)+(ka&65535)|0;sa=((f(G,N)|0)+(ca>>>16)|0)+(ka>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(y,P)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(G,P)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(y,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(y,Q)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(G,Q)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(y,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(y,R)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(G,R)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(y,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(y,S)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(G,S)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(y,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(y,T)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(G,T)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(y,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(y,U)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(G,U)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(y,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ka=wa;ra=((f(z,N)|0)+(da&65535)|0)+(la&65535)|0;sa=((f(H,N)|0)+(da>>>16)|0)+(la>>>16)|0;ta=((f(z,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(z,O)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(H,O)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(z,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(z,P)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(H,P)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(z,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(z,Q)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(H,Q)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(z,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(z,R)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(H,R)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(z,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(z,S)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(H,S)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(z,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(z,T)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(H,T)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(z,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(z,U)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(H,U)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(z,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;la=wa;ra=((f(A,N)|0)+(ea&65535)|0)+(ma&65535)|0;sa=((f(I,N)|0)+(ea>>>16)|0)+(ma>>>16)|0;ta=((f(A,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(A,O)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(I,O)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(A,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(A,P)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(I,P)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(A,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(A,Q)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(I,Q)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(A,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(A,R)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(I,R)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(A,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(A,S)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(I,S)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(A,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(A,T)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(I,T)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(A,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(A,U)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(I,U)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(A,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ma=wa;ra=((f(B,N)|0)+(fa&65535)|0)+(na&65535)|0;sa=((f(J,N)|0)+(fa>>>16)|0)+(na>>>16)|0;ta=((f(B,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(B,O)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(J,O)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(B,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(B,P)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(J,P)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(B,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(B,Q)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(J,Q)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(B,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(B,R)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(J,R)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(B,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(B,S)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(J,S)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(B,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(B,T)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(J,T)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(B,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(B,U)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(J,U)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(B,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;na=wa;ra=((f(C,N)|0)+(ga&65535)|0)+(oa&65535)|0;sa=((f(K,N)|0)+(ga>>>16)|0)+(oa>>>16)|0;ta=((f(C,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(C,O)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(K,O)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(C,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(C,P)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(K,P)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(C,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(C,Q)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(K,Q)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(C,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(C,R)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(K,R)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(C,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(C,S)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(K,S)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(C,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(C,T)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(K,T)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(C,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(C,U)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(K,U)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(C,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;oa=wa;ra=((f(D,N)|0)+(ha&65535)|0)+(pa&65535)|0;sa=((f(L,N)|0)+(ha>>>16)|0)+(pa>>>16)|0;ta=((f(D,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(D,O)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(L,O)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(D,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(D,P)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(L,P)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(D,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(D,Q)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(L,Q)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(D,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(D,R)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(L,R)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(D,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(D,S)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(L,S)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(D,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(D,T)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(L,T)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(D,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;ra=((f(D,U)|0)+(oa&65535)|0)+(wa&65535)|0;sa=((f(L,U)|0)+(oa>>>16)|0)+(wa>>>16)|0;ta=((f(D,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;oa=ta<<16|ra&65535;pa=wa;ra=((f(E,N)|0)+(ia&65535)|0)+(qa&65535)|0;sa=((f(M,N)|0)+(ia>>>16)|0)+(qa>>>16)|0;ta=((f(E,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(E,O)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(M,O)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(E,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(E,P)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(M,P)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(E,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(E,Q)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(M,Q)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(E,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(E,R)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(M,R)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(E,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(E,S)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(M,S)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(E,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;ra=((f(E,T)|0)+(oa&65535)|0)+(wa&65535)|0;sa=((f(M,T)|0)+(oa>>>16)|0)+(wa>>>16)|0;ta=((f(E,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;oa=ta<<16|ra&65535;ra=((f(E,U)|0)+(pa&65535)|0)+(wa&65535)|0;sa=((f(M,U)|0)+(pa>>>16)|0)+(wa>>>16)|0;ta=((f(E,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;pa=ta<<16|ra&65535;qa=wa;Da=ya+(Ba+Ca|0)|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ba&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((fa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(fa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ga&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ga>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ha&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ha>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ia&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ia>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16}Da=ya+(Ba+Ca|0)|0;xa=e[Ga+Da>>2]|0;ra=(((xa&65535)+((ja&65535)<<1)|0)+ua|0)+va|0;ta=((xa>>>16)+(ja>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ka&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ka>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((la&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(la>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ma&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ma>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((na&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(na>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((oa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(oa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((pa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(pa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((qa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(qa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;va=ta>>>16}for(Da=Da+4|0;!!va&(Da|0)>2]|0;ra=(xa&65535)+va|0;ta=(xa>>>16)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;va=ta>>>16}}}}function s(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0;j(v,u,y);for(Q=v-1&-4;(Q|0)>=0;Q=Q-4|0){A=e[u+Q>>2]|0;if(A){v=Q;break}}for(Q=x-1&-4;(Q|0)>=0;Q=Q-4|0){B=e[w+Q>>2]|0;if(B){x=Q;break}}while((B&2147483648)==0){B=B<<1;C=C+1|0}E=e[u+v>>2]|0;if(C)D=E>>>(32-C|0);for(Q=v-4|0;(Q|0)>=0;Q=Q-4|0){A=e[u+Q>>2]|0;e[y+Q+4>>2]=E<>>(32-C|0):0);E=A}e[y>>2]=E<>2]|0;for(Q=x-4|0;(Q|0)>=0;Q=Q-4|0){B=e[w+Q>>2]|0;e[w+Q+4>>2]=F<>>(32-C|0);F=B}e[w>>2]=F<>2]|0;G=F>>>16,H=F&65535;for(Q=v;(Q|0)>=(x|0);Q=Q-4|0){R=Q-x|0;E=e[y+Q>>2]|0;I=(D>>>0)/(G>>>0)|0,K=(D>>>0)%(G>>>0)|0,M=f(I,H)|0;while((I|0)==65536|M>>>0>(K<<16|E>>>16)>>>0){I=I-1|0,K=K+G|0,M=M-H|0;if((K|0)>=65536)break}O=0,P=0;for(S=0;(S|0)<=(x|0);S=S+4|0){B=e[w+S>>2]|0;M=(f(I,B&65535)|0)+(O>>>16)|0;N=(f(I,B>>>16)|0)+(M>>>16)|0;B=O&65535|M<<16;O=N;A=e[y+R+S>>2]|0;M=((A&65535)-(B&65535)|0)+P|0;N=((A>>>16)-(B>>>16)|0)+(M>>16)|0;e[y+R+S>>2]=N<<16|M&65535;P=N>>16}M=((D&65535)-(O&65535)|0)+P|0;N=((D>>>16)-(O>>>16)|0)+(M>>16)|0;e[y+R+S>>2]=D=N<<16|M&65535;P=N>>16;if(P){I=I-1|0,K=K-G|0;P=0;for(S=0;(S|0)<=(x|0);S=S+4|0){B=e[w+S>>2]|0;A=e[y+R+S>>2]|0;M=((A&65535)+(B&65535)|0)+P|0;N=((A>>>16)+(B>>>16)|0)+(M>>>16)|0;e[y+R+S>>2]=N<<16|M&65535;P=N>>>16}e[y+R+S>>2]=D=D+P|0}E=e[y+Q>>2]|0;A=D<<16|E>>>16;J=(A>>>0)/(G>>>0)|0,L=(A>>>0)%(G>>>0)|0,M=f(J,H)|0;while((J|0)==65536|M>>>0>(L<<16|E&65535)>>>0){J=J-1|0,L=L+G|0,M=M-H|0;if((L|0)>=65536)break}O=0,P=0;for(S=0;(S|0)<=(x|0);S=S+4|0){B=e[w+S>>2]|0;M=(f(J,B&65535)|0)+(O&65535)|0;N=((f(J,B>>>16)|0)+(M>>>16)|0)+(O>>>16)|0;B=M&65535|N<<16;O=N>>>16;A=e[y+R+S>>2]|0;M=((A&65535)-(B&65535)|0)+P|0;N=((A>>>16)-(B>>>16)|0)+(M>>16)|0;P=N>>16;e[y+R+S>>2]=N<<16|M&65535}M=((D&65535)-(O&65535)|0)+P|0;N=((D>>>16)-(O>>>16)|0)+(M>>16)|0;e[y+R+S>>2]=D=N<<16|M&65535;P=N>>16;if(P){J=J-1|0,L=L+G|0;P=0;for(S=0;(S|0)<=(x|0);S=S+4|0){B=e[w+S>>2]|0;A=e[y+R+S>>2]|0;M=((A&65535)+(B&65535)|0)+P|0;N=((A>>>16)+(B>>>16)|0)+(M>>>16)|0;P=N>>>16;e[y+R+S>>2]=M&65535|N<<16}e[y+R+S>>2]=D+P|0}e[z+R>>2]=I<<16|J;D=e[y+Q>>2]|0}if(C){E=e[y>>2]|0;for(Q=4;(Q|0)<=(x|0);Q=Q+4|0){A=e[y+Q>>2]|0;e[y+Q-4>>2]=A<<(32-C|0)|E>>>C;E=A}e[y+x>>2]=E>>>C}}function t(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;A=h(x<<1)|0;k(x<<1,0,A);j(v,u,A);for(L=0;(L|0)<(x|0);L=L+4|0){C=e[A+L>>2]|0,D=C&65535,C=C>>>16;F=y>>>16,E=y&65535;G=f(D,E)|0,H=((f(D,F)|0)+(f(C,E)|0)|0)+(G>>>16)|0;D=G&65535,C=H&65535;K=0;for(M=0;(M|0)<(x|0);M=M+4|0){N=L+M|0;F=e[w+M>>2]|0,E=F&65535,F=F>>>16;J=e[A+N>>2]|0;G=((f(D,E)|0)+(K&65535)|0)+(J&65535)|0;H=((f(D,F)|0)+(K>>>16)|0)+(J>>>16)|0;I=((f(C,E)|0)+(H&65535)|0)+(G>>>16)|0;K=((f(C,F)|0)+(I>>>16)|0)+(H>>>16)|0;J=I<<16|G&65535;e[A+N>>2]=J}N=L+M|0;J=e[A+N>>2]|0;G=((J&65535)+(K&65535)|0)+B|0;H=((J>>>16)+(K>>>16)|0)+(G>>>16)|0;e[A+N>>2]=H<<16|G&65535;B=H>>>16}j(x,A+x|0,z);i(x<<1);if(B|(m(w,x,z,x)|0)<=0){p(z,x,w,x,z,x)|0}}return{sreset:g,salloc:h,sfree:i,z:k,tst:n,neg:l,cmp:m,add:o,sub:p,mul:q,sqr:r,div:s,mredc:t}}function $a(a){return a instanceof _a}function _a(a){var b=Sc,c=0,d=0;if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),void 0===a);else if(m(a)){var e=Math.abs(a);e>4294967295?(b=new Uint32Array(2),b[0]=0|e,b[1]=e/4294967296|0,c=52):e>0?(b=new Uint32Array(1),b[0]=e,c=32):(b=Sc,c=0),d=0>a?-1:1}else if(p(a)){if(c=8*a.length,!c)return Uc;b=new Uint32Array(c+31>>5);for(var g=a.length-4;g>=0;g-=4)b[a.length-4-g>>2]=a[g]<<24|a[g+1]<<16|a[g+2]<<8|a[g+3];-3===g?b[b.length-1]=a[0]:-2===g?b[b.length-1]=a[0]<<8|a[1]:-1===g&&(b[b.length-1]=a[0]<<16|a[1]<<8|a[2]),d=1}else{if("object"!=typeof a||null===a)throw new TypeError("number is of unexpected type");b=new Uint32Array(a.limbs),c=a.bitLength,d=a.sign}this.limbs=b,this.bitLength=c,this.sign=d}function ab(a){a=a||16;var b=this.limbs,c=this.bitLength,e="";if(16!==a)throw new d("bad radix");for(var f=(c+31>>5)-1;f>=0;f--){var g=b[f].toString(16);e+="00000000".substr(g.length),e+=g}return e=e.replace(/^0+/,""),e.length||(e="0"),this.sign<0&&(e="-"+e),e}function bb(){var a=this.bitLength,b=this.limbs;if(0===a)return new Uint8Array(0);for(var c=a+7>>3,d=new Uint8Array(c),e=0;c>e;e++){var f=c-e-1;d[e]=b[f>>2]>>((3&f)<<3)}return d}function cb(){var a=this.limbs,b=this.bitLength,c=this.sign;if(!c)return 0;if(32>=b)return c*(a[0]>>>0);if(52>=b)return c*(4294967296*(a[1]>>>0)+(a[0]>>>0));var d,e,f=0;for(d=a.length-1;d>=0;d--)if(0!==(e=a[d])){for(;0===(e<>>0):c*(1048576*((a[d]<>>32-f:0))>>>0)+((a[d-1]<1?a[d-2]>>>32-f:0))>>>12))*Math.pow(2,32*d-f-52)}function db(a){var b=this.limbs,c=this.bitLength;if(a>=c)return this;var d=new _a,e=a+31>>5,f=a%32;return d.limbs=new Uint32Array(b.subarray(0,e)),d.bitLength=a,d.sign=this.sign,f&&(d.limbs[e-1]&=-1>>>32-f),d}function eb(a,b){if(!m(a))throw new TypeError("TODO");if(void 0!==b&&!m(b))throw new TypeError("TODO");var c=this.limbs,d=this.bitLength;if(0>a)throw new RangeError("TODO");if(a>=d)return Uc;(void 0===b||b>d-a)&&(b=d-a);var e,f=new _a,g=a>>5,h=a+b+31>>5,i=b+31>>5,j=a%32,k=b%32;if(e=new Uint32Array(i),j){for(var l=0;h-g-1>l;l++)e[l]=c[g+l]>>>j|c[g+l+1]<<32-j;e[l]=c[g+l]>>>j}else e.set(c.subarray(g,h));return k&&(e[i-1]&=-1>>>32-k),f.limbs=e,f.bitLength=b,f.sign=this.sign,f}function fb(){var a=new _a;return a.limbs=this.limbs,a.bitLength=this.bitLength,a.sign=-1*this.sign,a}function gb(a){$a(a)||(a=new _a(a));var b=this.limbs,c=b.length,d=a.limbs,e=d.length,f=0;return this.signa.sign?1:(Rc.set(b,0),Rc.set(d,c),f=Za.cmp(0,c<<2,c<<2,e<<2),f*this.sign)}function hb(a){if($a(a)||(a=new _a(a)),!this.sign)return a;if(!a.sign)return this;var b,c,d,e,f=this.bitLength,g=this.limbs,h=g.length,i=this.sign,j=a.bitLength,k=a.limbs,l=k.length,m=a.sign,n=new _a;b=(f>j?f:j)+(i*m>0?1:0),c=b+31>>5,Za.sreset();var o=Za.salloc(h<<2),p=Za.salloc(l<<2),q=Za.salloc(c<<2);return Za.z(q-o+(c<<2),0,o),Rc.set(g,o>>2),Rc.set(k,p>>2),i*m>0?(Za.add(o,h<<2,p,l<<2,q,c<<2),d=i):i>m?(e=Za.sub(o,h<<2,p,l<<2,q,c<<2),d=e?m:i):(e=Za.sub(p,l<<2,o,h<<2,q,c<<2),d=e?i:m),e&&Za.neg(q,c<<2,q,c<<2),0===Za.tst(q,c<<2)?Uc:(n.limbs=new Uint32Array(Rc.subarray(q>>2,(q>>2)+c)),n.bitLength=b,n.sign=d,n)}function ib(a){return $a(a)||(a=new _a(a)),this.add(a.negate())}function jb(a){if($a(a)||(a=new _a(a)),!this.sign||!a.sign)return Uc;var b,c,d=this.bitLength,e=this.limbs,f=e.length,g=a.bitLength,h=a.limbs,i=h.length,j=new _a;b=d+g,c=b+31>>5,Za.sreset();var k=Za.salloc(f<<2),l=Za.salloc(i<<2),m=Za.salloc(c<<2);return Za.z(m-k+(c<<2),0,k),Rc.set(e,k>>2),Rc.set(h,l>>2),Za.mul(k,f<<2,l,i<<2,m,c<<2),j.limbs=new Uint32Array(Rc.subarray(m>>2,(m>>2)+c)),j.sign=this.sign*a.sign,j.bitLength=b,j}function kb(){if(!this.sign)return Uc;var a,b,c=this.bitLength,d=this.limbs,e=d.length,f=new _a;a=c<<1,b=a+31>>5,Za.sreset();var g=Za.salloc(e<<2),h=Za.salloc(b<<2);return Za.z(h-g+(b<<2),0,g),Rc.set(d,g>>2),Za.sqr(g,e<<2,h),f.limbs=new Uint32Array(Rc.subarray(h>>2,(h>>2)+b)),f.bitLength=a,f.sign=1,f}function lb(a){$a(a)||(a=new _a(a));var b,c,d=this.bitLength,e=this.limbs,f=e.length,g=a.bitLength,h=a.limbs,i=h.length,j=Uc,k=Uc;Za.sreset();var l=Za.salloc(f<<2),m=Za.salloc(i<<2),n=Za.salloc(i<<2),o=Za.salloc(f<<2);return Za.z(o-l+(f<<2),0,l),Rc.set(e,l>>2),Rc.set(h,m>>2),Za.div(l,f<<2,m,i<<2,n,o),b=Za.tst(o,f<<2)>>2,b&&(j=new _a,j.limbs=new Uint32Array(Rc.subarray(o>>2,(o>>2)+b)),j.bitLength=b<<5>d?d:b<<5,j.sign=this.sign*a.sign),c=Za.tst(n,i<<2)>>2,c&&(k=new _a,k.limbs=new Uint32Array(Rc.subarray(n>>2,(n>>2)+c)),k.bitLength=c<<5>g?g:c<<5,k.sign=this.sign),{quotient:j,remainder:k}}function mb(a,b){var c,d,e,f,g=0>a?-1:1,h=0>b?-1:1,i=1,j=0,k=0,l=1;for(a*=g,b*=h,f=b>a,f&&(e=a,a=b,b=e,e=g,g=h,h=e),d=Math.floor(a/b),c=a-d*b;c;)e=i-d*j,i=j,j=e,e=k-d*l,k=l,l=e,a=b,b=c,d=Math.floor(a/b),c=a-d*b;return j*=g,l*=h,f&&(e=j,j=l,l=e),{gcd:b,x:j,y:l}}function nb(a,b){$a(a)||(a=new _a(a)),$a(b)||(b=new _a(b));var c=a.sign,d=b.sign;0>c&&(a=a.negate()),0>d&&(b=b.negate());var e=a.compare(b);if(0>e){var f=a;a=b,b=f,f=c,c=d,d=f}var g,h,i,j=Vc,k=Uc,l=b.bitLength,m=Uc,n=Vc,o=a.bitLength;for(g=a.divide(b);(h=g.remainder)!==Uc;)i=g.quotient,g=j.subtract(i.multiply(k).clamp(l)).clamp(l),j=k,k=g,g=m.subtract(i.multiply(n).clamp(o)).clamp(o),m=n,n=g,a=b,b=h,g=a.divide(b);if(0>c&&(k=k.negate()),0>d&&(n=n.negate()),0>e){var f=k;k=n,n=f}return{gcd:b,x:k,y:n}}function ob(){if(_a.apply(this,arguments),this.valueOf()<1)throw new RangeError;if(!(this.bitLength<=32)){var a;if(1&this.limbs[0]){var b=(this.bitLength+31&-32)+1,c=new Uint32Array(b+31>>5);c[c.length-1]=1,a=new _a,a.sign=1,a.bitLength=b,a.limbs=c;var d=mb(4294967296,this.limbs[0]).y;this.coefficient=0>d?-d:4294967296-d,this.comodulus=a,this.comodulusRemainder=a.divide(this).remainder,this.comodulusRemainderSquare=a.square().divide(this).remainder}}}function pb(a){return $a(a)||(a=new _a(a)),a.bitLength<=32&&this.bitLength<=32?new _a(a.valueOf()%this.valueOf()):a.compare(this)<0?a:a.divide(this).remainder}function qb(a){a=this.reduce(a);var b=nb(this,a);return 1!==b.gcd.valueOf()?null:(b=b.y,b.sign<0&&(b=b.add(this).clamp(this.bitLength)),b)}function rb(a,b){$a(a)||(a=new _a(a)),$a(b)||(b=new _a(b));for(var c=0,d=0;d>>=1;var f=8;b.bitLength<=4536&&(f=7),b.bitLength<=1736&&(f=6),b.bitLength<=630&&(f=5),b.bitLength<=210&&(f=4),b.bitLength<=60&&(f=3),b.bitLength<=12&&(f=2),1<=c&&(f=1),a=sb(this.reduce(a).multiply(this.comodulusRemainderSquare),this);var g=sb(a.square(),this),h=new Array(1<d;d++)h[d]=sb(h[d-1].multiply(g),this);for(var i=this.comodulusRemainder,j=i,d=b.limbs.length-1;d>=0;d--)for(var e=b.limbs[d],k=32;k>0;)if(2147483648&e){for(var l=e>>>32-f,m=f;0===(1&l);)l>>>=1,m--;for(var n=h[l>>>1];l;)l>>>=1,j!==i&&(j=sb(j.square(),this));j=j!==i?sb(j.multiply(n),this):n,e<<=m,k-=m}else j!==i&&(j=sb(j.square(),this)),e<<=1,k--;return j=sb(j,this)}function sb(a,b){var c=a.limbs,d=c.length,e=b.limbs,f=e.length,g=b.coefficient;Za.sreset();var h=Za.salloc(d<<2),i=Za.salloc(f<<2),j=Za.salloc(f<<2);Za.z(j-h+(f<<2),0,h),Rc.set(c,h>>2),Rc.set(e,i>>2),Za.mredc(h,d<<2,i,f<<2,g,j);var k=new _a;return k.limbs=new Uint32Array(Rc.subarray(j>>2,(j>>2)+f)),k.bitLength=b.bitLength,k.sign=1,k}function tb(a){var b=new _a(this),c=0;for(b.limbs[0]-=1;0===b.limbs[c>>5];)c+=32;for(;0===(b.limbs[c>>5]>>(31&c)&1);)c++;b=b.slice(c);for(var d=new ob(this),e=this.subtract(Vc),f=new _a(this),g=this.limbs.length-1;0===f.limbs[g];)g--;for(;--a>=0;){for(Wa(f.limbs),f.limbs[0]<2&&(f.limbs[0]+=2);f.compare(e)>=0;)f.limbs[g]>>>=1;var h=d.power(f,b);if(0!==h.compare(Vc)&&0!==h.compare(e)){for(var i=c;--i>0;){if(h=h.square().divide(d).remainder,0===h.compare(Vc))return!1;if(0===h.compare(e))break}if(0===i)return!1}}return!0}function ub(a){a=a||80;var b=this.limbs,c=0;if(0===(1&b[0]))return!1;if(1>=a)return!0;var d=0,e=0,f=0;for(c=0;c>>=2;for(var h=b[c];h;)e+=3&h,h>>>=2,e-=3&h,h>>>=2;for(var i=b[c];i;)f+=15&i,i>>>=4,f-=15&i,i>>>=4}return d%3&&e%5&&f%17?2>=a?!0:tb.call(this,a>>>1):!1}function vb(a){if(Xc.length>=a)return Xc.slice(0,a);for(var b=Xc[Xc.length-1]+2;Xc.length=d*d&&b%d!=0;d=Xc[++c]);d*d>b&&Xc.push(b)}return Xc}function wb(a,c){var d=a+31>>5,e=new _a({sign:1,bitLength:a,limbs:d}),f=e.limbs,g=1e4;512>=a&&(g=2200),256>=a&&(g=600);var h=vb(g),i=new Uint32Array(g),j=a*b.Math.LN2|0,k=27;for(a>=250&&(k=12),a>=450&&(k=6),a>=850&&(k=3),a>=1300&&(k=2);;){Wa(f),f[0]|=1,f[d-1]|=1<<(a-1&31),31&a&&(f[d-1]&=l(a+1&31)-1),i[0]=1;for(var m=1;g>m;m++)i[m]=e.divide(h[m]).remainder.valueOf();a:for(var n=0;j>n;n+=2,f[0]+=2){for(var m=1;g>m;m++)if((i[m]+n)%h[m]===0)continue a;if(("function"!=typeof c||c(e))&&tb.call(e,k))return e}}}function xb(a){a=a||{},this.key=null,this.result=null,this.reset(a)}function yb(a){a=a||{},this.result=null;var b=a.key;if(void 0!==b){if(!(b instanceof Array))throw new TypeError("unexpected key type");var c=b.length;if(2!==c&&3!==c&&8!==c)throw new SyntaxError("unexpected key type");var d=[];d[0]=new ob(b[0]),d[1]=new _a(b[1]),c>2&&(d[2]=new _a(b[2])),c>3&&(d[3]=new ob(b[3]),d[4]=new ob(b[4]),d[5]=new _a(b[5]),d[6]=new _a(b[6]),d[7]=new _a(b[7])),this.key=d}return this}function zb(a){if(!this.key)throw new c("no key is associated with the instance");n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a));var b;if(p(a))b=new _a(a);else{if(!$a(a))throw new TypeError("unexpected data type");b=a}if(this.key[0].compare(b)<=0)throw new RangeError("data too large");var d=this.key[0],e=this.key[1],g=d.power(b,e).toBytes(),h=d.bitLength+7>>3;if(g.length3){for(var e=this.key[0],g=this.key[3],h=this.key[4],i=this.key[5],j=this.key[6],k=this.key[7],l=g.power(b,i),m=h.power(b,j),q=l.subtract(m);q.sign<0;)q=q.add(g);var r=g.reduce(k.multiply(q));d=r.multiply(h).add(m).clamp(e.bitLength).toBytes()}else{var e=this.key[0],s=this.key[2];d=e.power(b,s).toBytes()}var t=e.bitLength+7>>3;if(d.lengtha)throw new d("bit length is too small");if(n(b)&&(b=f(b)),o(b)&&(b=new Uint8Array(b)),!(p(b)||m(b)||$a(b)))throw new TypeError("unexpected exponent type");if(b=new _a(b),0===(1&b.limbs[0]))throw new d("exponent must be an odd number");var c,b,e,g,h,i,j,k,l,q;g=wb(a>>1,function(a){return i=new _a(a),i.limbs[0]-=1,1==nb(i,b).gcd.valueOf()}),h=wb(a-(a>>1),function(d){return c=new ob(g.multiply(d)),c.limbs[(a+31>>5)-1]>>>(a-1&31)?(j=new _a(d),j.limbs[0]-=1,1==nb(j,b).gcd.valueOf()):!1}),e=new ob(i.multiply(j)).inverse(b),k=e.divide(i).remainder,l=e.divide(j).remainder,g=new ob(g),h=new ob(h);var q=g.inverse(h);return[c,b,e,g,h,k,l,q]}function Cb(a){if(a=a||{},!a.hash)throw new SyntaxError("option 'hash' is required");if(!a.hash.HASH_SIZE)throw new SyntaxError("option 'hash' supplied doesn't seem to be a valid hash function");this.hash=a.hash,this.label=null,this.reset(a)}function Db(a){a=a||{};var b=a.label;if(void 0!==b){if(o(b)||p(b))b=new Uint8Array(b);else{if(!n(b))throw new TypeError("unexpected label type");b=f(b)}this.label=b.length>0?b:null}else this.label=null;yb.call(this,a)}function Eb(a){if(!this.key)throw new c("no key is associated with the instance");var b=Math.ceil(this.key[0].bitLength/8),e=this.hash.HASH_SIZE,g=a.byteLength||a.length||0,h=b-g-2*e-2;if(g>b-2*this.hash.HASH_SIZE-2)throw new d("data too large");var i=new Uint8Array(b),j=i.subarray(1,e+1),k=i.subarray(e+1);if(p(a))k.set(a,e+h+1);else if(o(a))k.set(new Uint8Array(a),e+h+1);else{if(!n(a))throw new TypeError("unexpected data type");k.set(f(a),e+h+1)}k.set(this.hash.reset().process(this.label||"").finish().result,0),k[e+h]=1,Wa(j);for(var l=Gb.call(this,j,k.length),m=0;ml;l++)if(n[l]!==j[l])throw new e("decryption failed");for(var o=f;og;g++){e[0]=g>>>24,e[1]=g>>>16&255,e[2]=g>>>8&255,e[3]=255&g;var h=d.subarray(g*c),i=this.hash.reset().process(a).process(e).finish().result;i.length>h.length&&(i=i.subarray(0,h.length)),h.set(i)}return d}function Hb(a){if(a=a||{},!a.hash)throw new SyntaxError("option 'hash' is required");if(!a.hash.HASH_SIZE)throw new SyntaxError("option 'hash' supplied doesn't seem to be a valid hash function");this.hash=a.hash,this.saltLength=4,this.reset(a)}function Ib(a){a=a||{},yb.call(this,a);var b=a.saltLength;if(void 0!==b){if(!m(b)||0>b)throw new TypeError("saltLength should be a non-negative number");if(null!==this.key&&Math.ceil((this.key[0].bitLength-1)/8)0&&Wa(n),j[g]=1,k.set(n),i.set(this.hash.reset().process(l).finish().result);for(var o=Gb.call(this,i,j.length),p=0;p>>q),Ab.call(this,h),this}function Kb(a,b){if(!this.key)throw new c("no key is associated with the instance");var d=this.key[0].bitLength,f=this.hash.HASH_SIZE,g=Math.ceil((d-1)/8),h=this.saltLength,i=g-h-f-2;zb.call(this,a);var j=this.result;if(188!==j[g-1])throw new e("bad signature");var k=j.subarray(g-f-1,g-1),l=j.subarray(0,g-f-1),m=l.subarray(i+1),n=8*g-d+1;if(n%8&&j[0]>>>8-n)throw new e("bad signature");for(var o=Gb.call(this,k,l.length),p=0;p>>n);for(var p=0;i>p;p++)if(0!==l[p])throw new e("bad signature");if(1!==l[i])throw new e("bad signature");var q=new Uint8Array(8+f+h),r=q.subarray(8,8+f),s=q.subarray(8+f);r.set(this.hash.reset().process(b).finish().result),s.set(m);for(var t=this.hash.reset().process(q).finish().result,p=0;f>p;p++)if(k[p]!==t[p])throw new e("bad signature");return this}function Lb(a,b){if(void 0===a)throw new SyntaxError("bitlen required");if(void 0===b)throw new SyntaxError("e required");for(var c=Bb(a,b),d=0;da;a++)e[a]=c,b=128&c,c<<=1,c&=255,128===b&&(c^=27),c^=e[a],f[e[a]]=a;e[255]=e[0],f[0]=0,k=!0}function b(a,b){var c=e[(f[a]+f[b])%255];return(0===a||0===b)&&(c=0),c}function c(a){var b=e[255-f[a]];return 0===a&&(b=0),b}function d(){function d(a){var b,d,e;for(d=e=c(a),b=0;4>b;b++)d=255&(d<<1|d>>>7),e^=d;return e^=99}k||a(),g=[],h=[],i=[[],[],[],[]],j=[[],[],[],[]];for(var e=0;256>e;e++){var f=d(e);g[e]=f,h[f]=e,i[0][e]=b(2,f)<<24|f<<16|f<<8|b(3,f),j[0][f]=b(14,e)<<24|b(9,e)<<16|b(13,e)<<8|b(11,e);for(var l=1;4>l;l++)i[l][e]=i[l-1][e]>>>8|i[l-1][e]<<24,j[l][f]=j[l-1][f]>>>8|j[l-1][f]<<24}}var e,f,g,h,i,j,k=!1,l=!1,m=function(a,b,c){function e(a,b,c,d,e,h,i,k,l){var n=f.subarray(0,60),o=f.subarray(256,316);n.set([b,c,d,e,h,i,k,l]);for(var p=a,q=1;4*a+28>p;p++){var r=n[p-1];(p%a===0||8===a&&p%a===4)&&(r=g[r>>>24]<<24^g[r>>>16&255]<<16^g[r>>>8&255]<<8^g[255&r]),p%a===0&&(r=r<<8^r>>>24^q<<24,q=q<<1^(128&q?27:0)),n[p]=n[p-a]^r}for(var s=0;p>s;s+=4)for(var t=0;4>t;t++){var r=n[p-(4+s)+(4-t)%4];o[s+t]=4>s||s>=p-4?r:j[0][g[r>>>24]]^j[1][g[r>>>16&255]]^j[2][g[r>>>8&255]]^j[3][g[255&r]]}m.set_rounds(a+5)}l||d();var f=new Uint32Array(c);f.set(g,512),f.set(h,768);for(var k=0;4>k;k++)f.set(i[k],4096+1024*k>>2),f.set(j[k],8192+1024*k>>2);var m=function(a,b,c){"use asm";var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0;var y=new a.Uint32Array(c),z=new a.Uint8Array(c);function A(X,Y,Z,$,_,aa,ba,ca){X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;var da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0;da=Z|1024,ea=Z|2048,fa=Z|3072;_=_^y[(X|0)>>2],aa=aa^y[(X|4)>>2],ba=ba^y[(X|8)>>2],ca=ca^y[(X|12)>>2];for(ka=16;(ka|0)<=$<<4;ka=ka+16|0){ga=y[(Z|_>>22&1020)>>2]^y[(da|aa>>14&1020)>>2]^y[(ea|ba>>6&1020)>>2]^y[(fa|ca<<2&1020)>>2]^y[(X|ka|0)>>2],ha=y[(Z|aa>>22&1020)>>2]^y[(da|ba>>14&1020)>>2]^y[(ea|ca>>6&1020)>>2]^y[(fa|_<<2&1020)>>2]^y[(X|ka|4)>>2],ia=y[(Z|ba>>22&1020)>>2]^y[(da|ca>>14&1020)>>2]^y[(ea|_>>6&1020)>>2]^y[(fa|aa<<2&1020)>>2]^y[(X|ka|8)>>2],ja=y[(Z|ca>>22&1020)>>2]^y[(da|_>>14&1020)>>2]^y[(ea|aa>>6&1020)>>2]^y[(fa|ba<<2&1020)>>2]^y[(X|ka|12)>>2];_=ga,aa=ha,ba=ia,ca=ja}d=y[(Y|_>>22&1020)>>2]<<24^y[(Y|aa>>14&1020)>>2]<<16^y[(Y|ba>>6&1020)>>2]<<8^y[(Y|ca<<2&1020)>>2]^y[(X|ka|0)>>2],e=y[(Y|aa>>22&1020)>>2]<<24^y[(Y|ba>>14&1020)>>2]<<16^y[(Y|ca>>6&1020)>>2]<<8^y[(Y|_<<2&1020)>>2]^y[(X|ka|4)>>2],f=y[(Y|ba>>22&1020)>>2]<<24^y[(Y|ca>>14&1020)>>2]<<16^y[(Y|_>>6&1020)>>2]<<8^y[(Y|aa<<2&1020)>>2]^y[(X|ka|8)>>2],g=y[(Y|ca>>22&1020)>>2]<<24^y[(Y|_>>14&1020)>>2]<<16^y[(Y|aa>>6&1020)>>2]<<8^y[(Y|ba<<2&1020)>>2]^y[(X|ka|12)>>2]}function B(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,X,Y,Z,$)}function C(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0;A(1024,3072,8192,x,X,$,Z,Y);_=e,e=g,g=_}function D(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h^X,i^Y,j^Z,k^$);h=d,i=e,j=f,k=g}function E(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0;A(1024,3072,8192,x,X,$,Z,Y);_=e,e=g,g=_;d=d^h,e=e^i,f=f^j,g=g^k;h=X,i=Y,j=Z,k=$}function F(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);h=d=d^X,i=e=e^Y,j=f=f^Z,k=g=g^$}function G(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);d=d^X,e=e^Y,f=f^Z,g=g^$;h=X,i=Y,j=Z,k=$}function H(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);h=d,i=e,j=f,k=g;d=d^X,e=e^Y,f=f^Z,g=g^$}function I(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,l,m,n,o);o=~s&o|s&o+1,n=~r&n|r&n+((o|0)==0),m=~q&m|q&m+((n|0)==0),l=~p&l|p&l+((m|0)==0);d=d^X,e=e^Y,f=f^Z,g=g^$}function J(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0;X=X^h,Y=Y^i,Z=Z^j,$=$^k;_=t|0,aa=u|0,ba=v|0,ca=w|0;for(;(ha|0)<128;ha=ha+1|0){if(_>>>31){da=da^X,ea=ea^Y,fa=fa^Z,ga=ga^$}_=_<<1|aa>>>31,aa=aa<<1|ba>>>31,ba=ba<<1|ca>>>31,ca=ca<<1;ia=$&1;$=$>>>1|Z<<31,Z=Z>>>1|Y<<31,Y=Y>>>1|X<<31,X=X>>>1;if(ia)X=X^3774873600}h=da,i=ea,j=fa,k=ga}function K(X){X=X|0;x=X}function L(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;d=X,e=Y,f=Z,g=$}function M(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;h=X,i=Y,j=Z,k=$}function N(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;l=X,m=Y,n=Z,o=$}function O(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;p=X,q=Y,r=Z,s=$}function P(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;o=~s&o|s&$,n=~r&n|r&Z,m=~q&m|q&Y,l=~p&l|p&X}function Q(X){X=X|0;if(X&15)return-1;z[X|0]=d>>>24,z[X|1]=d>>>16&255,z[X|2]=d>>>8&255,z[X|3]=d&255,z[X|4]=e>>>24,z[X|5]=e>>>16&255,z[X|6]=e>>>8&255,z[X|7]=e&255,z[X|8]=f>>>24,z[X|9]=f>>>16&255,z[X|10]=f>>>8&255,z[X|11]=f&255,z[X|12]=g>>>24,z[X|13]=g>>>16&255,z[X|14]=g>>>8&255,z[X|15]=g&255;return 16}function R(X){X=X|0;if(X&15)return-1;z[X|0]=h>>>24,z[X|1]=h>>>16&255,z[X|2]=h>>>8&255,z[X|3]=h&255,z[X|4]=i>>>24,z[X|5]=i>>>16&255,z[X|6]=i>>>8&255,z[X|7]=i&255,z[X|8]=j>>>24,z[X|9]=j>>>16&255,z[X|10]=j>>>8&255,z[X|11]=j&255,z[X|12]=k>>>24,z[X|13]=k>>>16&255,z[X|14]=k>>>8&255,z[X|15]=k&255;return 16}function S(){B(0,0,0,0);t=d,u=e,v=f,w=g}function T(X,Y,Z){X=X|0;Y=Y|0;Z=Z|0;var $=0;if(Y&15)return-1;while((Z|0)>=16){V[X&7](z[Y|0]<<24|z[Y|1]<<16|z[Y|2]<<8|z[Y|3],z[Y|4]<<24|z[Y|5]<<16|z[Y|6]<<8|z[Y|7],z[Y|8]<<24|z[Y|9]<<16|z[Y|10]<<8|z[Y|11],z[Y|12]<<24|z[Y|13]<<16|z[Y|14]<<8|z[Y|15]);z[Y|0]=d>>>24,z[Y|1]=d>>>16&255,z[Y|2]=d>>>8&255,z[Y|3]=d&255,z[Y|4]=e>>>24,z[Y|5]=e>>>16&255,z[Y|6]=e>>>8&255,z[Y|7]=e&255,z[Y|8]=f>>>24,z[Y|9]=f>>>16&255,z[Y|10]=f>>>8&255,z[Y|11]=f&255,z[Y|12]=g>>>24,z[Y|13]=g>>>16&255,z[Y|14]=g>>>8&255,z[Y|15]=g&255;$=$+16|0,Y=Y+16|0,Z=Z-16|0}return $|0}function U(X,Y,Z){X=X|0;Y=Y|0;Z=Z|0;var $=0;if(Y&15)return-1;while((Z|0)>=16){W[X&1](z[Y|0]<<24|z[Y|1]<<16|z[Y|2]<<8|z[Y|3],z[Y|4]<<24|z[Y|5]<<16|z[Y|6]<<8|z[Y|7],z[Y|8]<<24|z[Y|9]<<16|z[Y|10]<<8|z[Y|11],z[Y|12]<<24|z[Y|13]<<16|z[Y|14]<<8|z[Y|15]);$=$+16|0,Y=Y+16|0,Z=Z-16|0}return $|0}var V=[B,C,D,E,F,G,H,I];var W=[D,J];return{set_rounds:K,set_state:L,set_iv:M,set_nonce:N,set_mask:O,set_counter:P,get_state:Q,get_iv:R,gcm_init:S,cipher:T,mac:U}}(a,b,c);return m.set_key=e,m};return m.ENC={ECB:0,CBC:2,CFB:4,OFB:6,CTR:7},m.DEC={ECB:1,CBC:3,CFB:5,OFB:6,CTR:7},m.MAC={CBC:0,GCM:1},m.HEAP_DATA=16384,Object.freeze(m),m}(),Wb=C.prototype;Wb.BLOCK_SIZE=16,Wb.reset=x,Wb.encrypt=z,Wb.decrypt=B;var Xb=D.prototype;Xb.BLOCK_SIZE=16,Xb.reset=x,Xb.process=y,Xb.finish=z;var Yb=E.prototype;Yb.BLOCK_SIZE=16,Yb.reset=x,Yb.process=A,Yb.finish=B;var Zb=F.prototype;Zb.BLOCK_SIZE=16,Zb.reset=I,Zb.encrypt=z,Zb.decrypt=z;var $b=G.prototype;$b.BLOCK_SIZE=16,$b.reset=I,$b.process=y,$b.finish=z;var _b=68719476704,ac=K.prototype;ac.BLOCK_SIZE=16,ac.reset=N,ac.encrypt=Q,ac.decrypt=T;var bc=L.prototype;bc.BLOCK_SIZE=16,bc.reset=N,bc.process=O,bc.finish=P;var cc=M.prototype;cc.BLOCK_SIZE=16,cc.reset=N,cc.process=R,cc.finish=S;var dc=new Uint8Array(1048576),ec=Vb(b,null,dc.buffer);a.AES_CBC=C,a.AES_CBC.encrypt=U,a.AES_CBC.decrypt=V,a.AES_CBC.Encrypt=D,a.AES_CBC.Decrypt=E,a.AES_GCM=K,a.AES_GCM.encrypt=W,a.AES_GCM.decrypt=X,a.AES_GCM.Encrypt=L,a.AES_GCM.Decrypt=M;var fc=64,gc=20;aa.BLOCK_SIZE=fc,aa.HASH_SIZE=gc;var hc=aa.prototype;hc.reset=Y,hc.process=Z,hc.finish=$;var ic=null;aa.bytes=ca,aa.hex=da,aa.base64=ea,a.SHA1=aa;var jc=64,kc=32;ga.BLOCK_SIZE=jc,ga.HASH_SIZE=kc;var lc=ga.prototype;lc.reset=Y,lc.process=Z,lc.finish=$;var mc=null;ga.bytes=ia,ga.hex=ja,ga.base64=ka,a.SHA256=ga;var nc=la.prototype;nc.reset=oa,nc.process=pa,nc.finish=qa,ra.BLOCK_SIZE=aa.BLOCK_SIZE,ra.HMAC_SIZE=aa.HASH_SIZE;var oc=ra.prototype;oc.reset=sa,oc.process=pa,oc.finish=ta;var pc=null;va.BLOCK_SIZE=ga.BLOCK_SIZE,va.HMAC_SIZE=ga.HASH_SIZE;var qc=va.prototype;qc.reset=wa,qc.process=pa,qc.finish=xa;var rc=null;a.HMAC=la,ra.bytes=za,ra.hex=Aa,ra.base64=Ba,a.HMAC_SHA1=ra,va.bytes=Ca,va.hex=Da,va.base64=Ea,a.HMAC_SHA256=va;var sc=Fa.prototype;sc.reset=Ga,sc.generate=Ha;var tc=Ia.prototype;tc.reset=Ga,tc.generate=Ja;var uc=null,vc=La.prototype;vc.reset=Ga,vc.generate=Ma;var wc=null;a.PBKDF2=a.PBKDF2_HMAC_SHA1={bytes:Oa,hex:Pa,base64:Qa},a.PBKDF2_HMAC_SHA256={bytes:Ra,hex:Sa,base64:Ta};var xc,yc=function(){function a(){function a(){b^=d<<11,l=l+b|0,d=d+f|0,d^=f>>>2,m=m+d|0,f=f+l|0,f^=l<<8,n=n+f|0,l=l+m|0,l^=m>>>16,o=o+l|0,m=m+n|0,m^=n<<10,p=p+m|0,n=n+o|0,n^=o>>>4,b=b+n|0,o=o+p|0,o^=p<<8,d=d+o|0,p=p+b|0,p^=b>>>9,f=f+p|0,b=b+d|0}var b,d,f,l,m,n,o,p;h=i=j=0,b=d=f=l=m=n=o=p=2654435769;for(var q=0;4>q;q++)a();for(var q=0;256>q;q+=8)b=b+g[0|q]|0,d=d+g[1|q]|0,f=f+g[2|q]|0,l=l+g[3|q]|0,m=m+g[4|q]|0,n=n+g[5|q]|0,o=o+g[6|q]|0,p=p+g[7|q]|0,a(),e.set([b,d,f,l,m,n,o,p],q);for(var q=0;256>q;q+=8)b=b+e[0|q]|0,d=d+e[1|q]|0,f=f+e[2|q]|0,l=l+e[3|q]|0,m=m+e[4|q]|0,n=n+e[5|q]|0,o=o+e[6|q]|0,p=p+e[7|q]|0,a(),e.set([b,d,f,l,m,n,o,p],q);c(1),k=256}function b(b){var c,d,e,h,i;if(q(b))b=new Uint8Array(b.buffer);else if(m(b))h=new Ub(1),h[0]=b,b=new Uint8Array(h.buffer);else if(n(b))b=f(b);else{if(!o(b))throw new TypeError("bad seed type");b=new Uint8Array(b)}for(i=b.length,d=0;i>d;d+=1024){for(e=d,c=0;1024>c&&i>e;e=d|++c)g[c>>2]^=b[e]<<((3&c)<<3);a()}}function c(a){a=a||1;for(var b,c,d;a--;)for(j=j+1|0,i=i+j|0,b=0;256>b;b+=4)h^=h<<13,h=e[b+128&255]+h|0,c=e[0|b],e[0|b]=d=e[c>>>2&255]+(h+i|0)|0,g[0|b]=i=e[d>>>10&255]+c|0,h^=h>>>6,h=e[b+129&255]+h|0,c=e[1|b],e[1|b]=d=e[c>>>2&255]+(h+i|0)|0,g[1|b]=i=e[d>>>10&255]+c|0,h^=h<<2,h=e[b+130&255]+h|0,c=e[2|b],e[2|b]=d=e[c>>>2&255]+(h+i|0)|0,g[2|b]=i=e[d>>>10&255]+c|0,h^=h>>>16,h=e[b+131&255]+h|0,c=e[3|b],e[3|b]=d=e[c>>>2&255]+(h+i|0)|0,g[3|b]=i=e[d>>>10&255]+c|0}function d(){return k--||(c(1),k=255),g[k]}var e=new Uint32Array(256),g=new Uint32Array(256),h=0,i=0,j=0,k=0;return{seed:b,prng:c,rand:d}}(),zc=b.console,Ac=b.Date.now,Bc=b.Math.random,Cc=b.performance,Dc=b.crypto||b.msCrypto;void 0!==Dc&&(xc=Dc.getRandomValues);var Ec,Fc=yc.rand,Gc=yc.seed,Hc=0,Ic=!1,Jc=!1,Kc=0,Lc=256,Mc=!1,Nc=!1,Oc={};if(void 0!==Cc)Ec=function(){return 1e3*Cc.now()|0};else{var Pc=1e3*Ac()|0;Ec=function(){return 1e3*Ac()-Pc|0}}a.random=Xa,a.random.seed=Va,Object.defineProperty(Xa,"allowWeak",{get:function(){return Mc},set:function(a){Mc=a}}),Object.defineProperty(Xa,"skipSystemRNGWarning",{get:function(){return Nc},set:function(a){Nc=a}}),a.getRandomValues=Wa,a.getRandomValues.seed=Va,Object.defineProperty(Wa,"allowWeak",{get:function(){return Mc},set:function(a){Mc=a}}),Object.defineProperty(Wa,"skipSystemRNGWarning",{get:function(){return Nc},set:function(a){Nc=a}}),b.Math.random=Xa,void 0===b.crypto&&(b.crypto={}),b.crypto.getRandomValues=Wa;var Qc;Qc=void 0===b.Math.imul?function(a,c,d){b.Math.imul=Ya;var e=Za(a,c,d);return delete b.Math.imul,e}:Za;var Rc=new Uint32Array(1048576),Za=Qc(b,null,Rc.buffer),Sc=new Uint32Array(0),Tc=_a.prototype=new Number;Tc.toString=ab,Tc.toBytes=bb,Tc.valueOf=cb,Tc.clamp=db,Tc.slice=eb,Tc.negate=fb,Tc.compare=gb,Tc.add=hb,Tc.subtract=ib,Tc.multiply=jb,Tc.square=kb,Tc.divide=lb;var Uc=new _a(0),Vc=new _a(1);Object.freeze(Uc),Object.freeze(Vc);var Wc=ob.prototype=new _a;Wc.reduce=pb,Wc.inverse=qb,Wc.power=rb;var Xc=[2,3];Tc.isProbablePrime=ub,_a.randomProbablePrime=wb,_a.ZERO=Uc,_a.ONE=Vc,_a.extGCD=nb,a.BigNumber=_a,a.Modulus=ob;var Yc=xb.prototype;Yc.reset=yb,Yc.encrypt=zb,Yc.decrypt=Ab,xb.generateKey=Bb;var Zc=Cb.prototype;Zc.reset=Db,Zc.encrypt=Eb,Zc.decrypt=Fb;var $c=Hb.prototype;$c.reset=Ib,$c.sign=Jb,$c.verify=Kb,a.RSA={generateKey:Lb},a.RSA_OAEP=Cb,a.RSA_OAEP_SHA1={encrypt:Mb,decrypt:Nb},a.RSA_OAEP=Cb,a.RSA_OAEP_SHA256={encrypt:Ob,decrypt:Pb},a.RSA_PSS=Hb,a.RSA_PSS_SHA1={sign:Qb,verify:Rb},a.RSA_PSS=Hb,a.RSA_PSS_SHA256={sign:Sb,verify:Tb}}({},function(){return this}()); +//# sourceMappingURL=asmcrypto.js.map + +var util = {}; +util.Base64 = { + _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", + stringify: CryptoJS.enc.Base64.stringify, + parse: CryptoJS.enc.Base64.parse +}; +util.salt = function (len) { + return Array.apply(0, new Array(len)).map(function () { + return (function (charset) { + return charset.charAt(Math.floor(Math.random() * charset.length)); + }('abcdefghijklmnopqrstuvwxyz0123456789')); + }).join(''); +}; + +var GMusicResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Google Play Music', + icon: '../images/icon.png', + weight: 90, + timeout: 8 + }, + + _authUrl: 'https://android.clients.google.com/auth', + _userAgent: 'tomahawk-gmusic-0.6.0', + _baseURL: 'https://www.googleapis.com/sj/v1/', + _webURL: 'https://play.google.com/music/', + // Google Play Services key version 7.3.29: + _googlePlayKey: "AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3" + + "srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax" + + "/6rmf5AAAAAwEAAQ==", + + getConfigUi: function () { + return { + "widget": Tomahawk.readBase64("config.ui"), + fields: [{ + name: "email", + widget: "email_edit", + property: "text" + }, { + name: "password", + widget: "password_edit", + property: "text" + }], + images: [{ + "play-logo.png": Tomahawk.readBase64("play-logo.png") + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + type: "textview", + text: "For this plug-in to work you must first login using the official Google Music " + + "iOS or Android app and play a song. After you've done that Tomahawk should then be " + + "able to authenticate with your account." + }, + { + type: "textview", + text: "Note: If you use 2-Step Verification, then you must create an " + + "app-specific " + + "password to use in Tomahawk. Otherwise, make sure that you enable " + + "\"less secure apps\" in your " + + "Google account " + + "settings" + }, + { + id: "email", + type: "textfield", + label: "E-Mail" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + } + ], + + newConfigSaved: function (newConfig) { + if (this._email !== newConfig.email + || this._password !== newConfig.password + || this._token !== newConfig.token) { + Tomahawk.log("Invalidating cache"); + var that = this; + gmusicCollection.wipe({id: gmusicCollection.settings.id}).then(function () { + window.localStorage.removeItem("gmusic_last_cache_update"); + that.init(); + }); + } + }, + + init: function () { + var name = this.settings.name; + var config = this.getUserConfig(); + if (!config.email || (!config.token && !config.password)) { + Tomahawk.PluginManager.unregisterPlugin("collection", gmusicCollection); + Tomahawk.log(name + " resolver not configured."); + return; + } + + this._email = config.email; + this._password = config.password; + this._token = config.token; + + // load signing key + var s1 = CryptoJS.enc.Base64.parse( + 'VzeC4H4h+T2f0VI180nVX8x+Mb5HiTtGnKgH52Otj8ZCGDz9jRW' + + 'yHb6QXK0JskSiOgzQfwTY5xgLLSdUSreaLVMsVVWfxfa8Rw=='); + var s2 = CryptoJS.enc.Base64.parse( + 'ZAPnhUkYwQ6y5DdQxWThbvhJHN8msQ1rqJw0ggKdufQjelrKuiG' + + 'GJI30aswkgCWTDyHkTGK9ynlqTkJ5L4CiGGUabGeo8M6JTQ=='); + for (var idx = 0; idx < s1.words.length; idx++) { + s1.words[idx] ^= s2.words[idx]; + } + this._key = s1; + + var that = this; + + var promise; + if (config.token) { + // The token has already been provided in the config. We don't need to login first. + promise = that._loadWebToken(config.token).then(function (webToken) { + return that._loadSettings(webToken, config.token); + }); + } else { + // No token provided in the config. We need to login with the given creds and fetch it + // first. + promise = that._login(config.email, config.password).then(function (token) { + that._token = token; + return that._loadWebToken(token).then(function (webToken) { + return that._loadSettings(webToken, token); + }); + }); + } + promise.then(function () { + return that._ensureCollection(); + }).then(function () { + that._ready = true; + }); + }, + + _convertTrack: function (entry) { + var realId; + if (entry.id) { + realId = entry.id; + } else { + realId = entry.nid; + } + + return { + artist: entry.artist, + album: entry.album, + track: entry.title, + year: entry.year, + + albumpos: entry.trackNumber, + discnumber: entry.discNumber, + + size: entry.estimatedSize, + duration: entry.durationMillis / 1000, + + source: "Google Music", + url: 'gmusic://track/' + realId, + checked: true + }; + }, + + _convertAlbum: function (entry) { + return { + artist: entry.artist, + album: entry.album, + year: entry.year + }; + }, + + _ensureCollection: function () { + var that = this; + + return gmusicCollection.revision({ + id: gmusicCollection.settings.id + }).then(function (result) { + var url = that._baseURL + + 'trackfeed?fields=nextPageToken,' + + 'data/items(id,nid,artist,album,title,year,trackNumber,discNumber,estimatedSize,durationMillis)'; + var lastCollectionUpdate = window.localStorage["gmusic_last_collection_update"]; + if (lastCollectionUpdate && lastCollectionUpdate == result) { + Tomahawk.log("Collection database has not been changed since last time."); + if (window.localStorage["gmusic_last_cache_update"]) { + url += '&updated-min=' + window.localStorage["gmusic_last_cache_update"] * 1000; + } + return that._fetchAndStoreCollection(url); + } else { + Tomahawk.log("Collection database has been changed. Wiping and re-fetching..."); + return gmusicCollection.wipe({ + id: gmusicCollection.settings.id + }).then(function () { + return that._fetchAndStoreCollection(url); + }); + } + }); + }, + + _fetchAndStoreCollection: function (url) { + var that = this; + var time = Date.now(); + if (!that._requestPromise) { + Tomahawk.log("Checking if collection needs to be updated"); + Tomahawk.PluginManager.registerPlugin("collection", gmusicCollection); + that._requestPromise = that._paginatedRequest(url).then(function (results) { + if (results && results.length > 0) { + Tomahawk.log("Collection needs to be updated"); + + var tracks = results.map(function (item) { + return that._convertTrack(item); + }); + gmusicCollection.addTracks({ + id: gmusicCollection.settings.id, + tracks: tracks + }).then(function (newRevision) { + Tomahawk.log("Updated cache in " + (Date.now() - time) + "ms"); + window.localStorage["gmusic_last_cache_update"] = Date.now(); + window.localStorage["gmusic_last_collection_update"] = newRevision; + }); + } else { + Tomahawk.log("Collection doesn't need to be updated"); + gmusicCollection.addTracks({ + id: gmusicCollection.settings.id, + tracks: [] + }); + } + }, function (xhr) { + Tomahawk.log("paginatedRequest failed: " + xhr.status + " - " + + xhr.statusText + " - " + xhr.responseText); + Tomahawk.PluginManager.unregisterPlugin("collection", gmusicCollection); + }).finally(function () { + that._requestPromise = undefined; + }); + } + return that._requestPromise; + }, + + _paginatedRequest: function (url, results, nextPageToken) { + var that = this; + var settings = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'GoogleLogin auth=' + this._token + } + }; + if (nextPageToken) { + settings.data = { + 'start-token': nextPageToken + }; + settings.dataFormat = 'json'; + } + results = results || []; + return Tomahawk.post(url, settings).then(function (response) { + if (response.data) { + results = results.concat(response.data.items); + Tomahawk.log("Received chunk of tracks, tracks total: " + results.length); + } + if (response.nextPageToken) { + return that._paginatedRequest(url, results, response.nextPageToken); + } else { + return results; + } + }); + }, + + _execSearchAllAccess: function (query, max_results) { + var that = this; + var url = this._baseURL + "query"; + var settings = { + data: { + q: query + }, + headers: { + Authorization: 'GoogleLogin auth=' + this._token + } + }; + if (max_results) { + settings.data["max-results"] = max_results; + } + + var time = Date.now(); + return Tomahawk.get(url, settings).then(function (response) { + var results = {tracks: [], albums: [], artists: []}; + // entries member is missing when there are no results + if (!response.entries) { + return results; + } + + var artistMap = {}, albumMap = {}; + for (var idx = 0; idx < response.entries.length; idx++) { + var entry = response.entries[idx]; + switch (entry.type) { + case '1': + results.tracks.push(that._convertTrack(entry.track)); + break; + case '2': + artistMap[entry.artist] = true; + break; + case '3': + albumMap[entry.artist + "♣" + entry.album + "♣" + entry.year] + = that._convertAlbum(entry); + break; + } + } + results.artists = Object.keys(artistMap); + var keys = Object.keys(albumMap); + results.albums = keys.map(function (key) { + return albumMap[key]; + }); + Tomahawk.log("All Access: Searched with query '" + query + "' for " + + (Date.now() - time) + "ms and found " + results.tracks.length + " track results"); + return results; + }, function (xhr) { + Tomahawk.log("Google Music search '" + query + "' failed:\n" + + xhr.status + " " + xhr.statusText.trim() + "\n" + + xhr.responseText.trim() + ); + }) + }, + + search: function (params) { + var query = params.query; + + if (!this._ready) { + return; + } + + if (this._allAccess) { + return this._execSearchAllAccess(query, 20); + } else { + return []; + } + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + if (!this._ready) { + return; + } + + if (this._allAccess) { + var time = Date.now(); + // Format the search as track-artists-album for now + var query = artist; + if (album) { + query += ' - ' + album; + } + query += ' - ' + track; + return this._execSearchAllAccess(query, 1).then(function (results) { + Tomahawk.log("All Access: Resolved track '" + artist + " - " + track + " - " + + album + "' for " + (Date.now() - time) + "ms and found " + + results.tracks.length + " track results"); + return results.tracks; + }); + } else { + return []; + } + }, + + _parseUrn: function (urn) { + var match = urn.match(/^gmusic:\/\/([a-z]+)\/(.+)$/); + if (!match) { + return null; + } + + return { + type: match[1], + id: match[2] + }; + }, + + getStreamUrl: function (params) { + var url = params.url; + + if (!this._ready) { + Tomahawk.log("Failed to get stream for '" + url + "', resolver wasn't ready"); + return; + } + var urn = this._parseUrn(url); + if (!urn || 'track' != urn.type) { + Tomahawk.log("Failed to get stream. Couldn't parse '" + urn + "'"); + return; + } + Tomahawk.log("Getting stream for '" + urn + "', track ID is '" + urn.id + "'"); + + var salt = util.salt(13); + var sig = CryptoJS.HmacSHA1(urn.id + salt, this._key).toString(util.Base64) + .replace(/=+$/, '') // no padding + .replace(/\+/g, '-') // URL-safe alphabet + .replace(/\//g, '_'); // URL-safe alphabet + + return { + url: 'https://android.clients.google.com/music/mplay' + + '?net=wifi&pt=e&targetkbps=8310' + + '&' + ('T' == urn.id[0] ? 'mjck' : 'songid') + + '=' + urn.id + '&slt=' + salt + '&sig=' + sig, + headers: { + 'Content-type': 'application/x-www-form-urlencoded', + 'Authorization': 'GoogleLogin auth=' + this._token, + 'X-Device-ID': this._deviceId + } + } + }, + + _loadSettings: function (webToken, token) { + var that = this; + + var url = that._webURL + 'services/fetchsettings'; + var settings = { + data: { + u: 0, + xt: webToken + }, + dataType: 'json', + headers: { + 'Authorization': 'GoogleLogin auth=' + token + } + }; + return Tomahawk.post(url, settings).then(function (response) { + if (!response.settings) { + Tomahawk.log("Wasn't able to get resolver settings"); + throw new Error("Wasn't able to get resolver settings"); + } + + that._allAccess = response.settings.entitlementInfo.isSubscription; + Tomahawk.log("Google Play Music All Access is " + + (that._allAccess ? "enabled" : "disabled" ) + ); + + for (var i = 0; i < response.settings.uploadDevice.length; i++) { + var device = response.settings.uploadDevice[i]; + if (2 == device.deviceType) { + // We have an Android device id + that._deviceId = device.id.slice(2); //remove prepended "0x" + Tomahawk.log(that.settings.name + " using Android device ID '" + + that._deviceId + "' from " + device.carrier + " " + + device.manufacturer + " " + device.model); + return; + } else if (3 == device.deviceType) { + // We have an iOS device id + that._deviceId = device.id; + Tomahawk.log(that.settings.name + " using iOS device ID '" + + that._deviceId + "' from " + device.name); + return; + } + } + + Tomahawk.log("There aren't any Android/iOS devices associated with your Google " + + "account. This resolver needs an Android/iOS device ID to function. Please " + + "open the Google Music application on an Android/iOS device and log in to " + + "your account."); + throw new Error("No Android/iOS devices associated with Google account." + + " Please open the 'Play Music' App, log in and play a song"); + }); + }, + + _loadWebToken: function (token) { + var that = this; + + var url = that._webURL + 'listen'; + var settings = { + type: 'HEAD', + needCookieHeader: true, + rawResponse: true, + headers: { + 'Authorization': 'GoogleLogin auth=' + token + } + }; + return Tomahawk.ajax(url, settings).then(function (request) { + var match = request.getResponseHeader('Set-Cookie').match(/^xt=([^;]+)(?:;|$)/m); + if (match) { + return match[1]; + } else { + Tomahawk.log("xt cookie missing"); + throw new Error("Wasn't able to get web token"); + } + }); + }, + + /** Asynchronously authenticates with the SkyJam service. + * Only one login attempt will run at a time. If a login request is + * already pending the callback (if one is provided) will be queued + * to run when it is complete. + */ + _login: function (email, password) { + var that = this; + + var url = this._authUrl; + var settings = { + data: { + "accountType": "HOSTED_OR_GOOGLE", + "Email": email.trim(), + "has_permission": 1, + "add_account": 1, + "EncryptedPasswd": that._buildSignature(email.trim(), password.trim()), + "service": "ac2dm", + "source": "android", + "device_country": "us", + "operatorCountry": "us", + "lang": "en", + "sdk_version": "17" + }, + headers: { + 'User-Agent': that._userAgent + } + }; + + if (!this._loginPromise) { + this._loginPromise = Tomahawk.post(url, settings).then(function (response) { + var parsedRes = that._parseAuthResponse(response); + if (!parsedRes['Token']) { + throw new Error("There's no 'Token' in the response"); + } + + var settings = { + data: { + "accountType": "HOSTED_OR_GOOGLE", + "Email": email.trim(), + "has_permission": 1, + "EncryptedPasswd": parsedRes['Token'], + "service": "sj", + "source": "android", + "app": "com.google.android.music", + "client_sig": "38918a453d07199354f8b19af05ec6562ced5788", + "device_country": "us", + "operatorCountry": "us", + "lang": "en", + "sdk_version": "17" + }, + headers: { + 'User-Agent': that._userAgent + } + }; + return Tomahawk.post(url, settings).then(function (response) { + var parsedRes = that._parseAuthResponse(response); + if (!parsedRes['Auth']) { + throw new Error("There's no 'Auth' in the response"); + } + Tomahawk.log("Google Play Music logged in successfully"); + return parsedRes['Auth']; + }); + }).finally(function () { + that._loginPromise = undefined; + }); + } + return this._loginPromise; + }, + + testConfig: function (config) { + var that = this; + + var promise; + if (config.token) { + // The token has already been provided in the config. We don't need to login first. + promise = that._loadWebToken(config.token).then(function (webToken) { + return that._loadSettings(webToken, config.token); + }); + } else { + // No token provided in the config. We need to login with the given creds and fetch it + // first. + promise = that._login(config.email, config.password).then(function (token) { + return that._loadWebToken(token).then(function (webToken) { + return that._loadSettings(webToken, token); + }); + }); + } + return promise.then(function () { + return Tomahawk.ConfigTestResultType.Success; + }, function (error) { + if (error instanceof Error) { + return error.message; + } else if (error && error.status == 403) { + return Tomahawk.ConfigTestResultType.InvalidCredentials; + } else { + return Tomahawk.ConfigTestResultType.CommunicationError; + } + }); + }, + + _parseAuthResponse: function (res) { + parsedRes = {}; + var lines = res.split("\n"); + for (var i = 0; i < lines.length; i++) { + if (!lines[i]) { + continue; + } + var parts = lines[i].split("="); + parsedRes[parts[0]] = parts[1]; + } + return parsedRes; + }, + + /** + * Author: jonleighton - https://gist.github.com/jonleighton/958841 + */ + _arrayBufferToBase64: function (arrayBuffer) { + var base64 = ''; + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + + var bytes = new Uint8Array(arrayBuffer); + var byteLength = bytes.byteLength; + var byteRemainder = byteLength % 3; + var mainLength = byteLength - byteRemainder; + + var a, b, c, d; + var chunk; + + // Main loop deals with bytes in chunks of 3 + for (var i = 0; i < mainLength; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + d = chunk & 63; // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] + } + + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength]; + + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4;// 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '==' + } else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; + + a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2;// 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '=' + } + + return base64 + }, + + _buildSignature: function (email, password) { + var buffer = new ArrayBuffer(133); + var signature = new Uint8Array(buffer); + + var keyBytes = asmCrypto.base64_to_bytes(this._googlePlayKey); + + var hashBytes = asmCrypto.SHA1.bytes(keyBytes); + // 0 is always the first element + signature[0] = 0; + // the elements' next 4 bytes are set to the first 4 bytes of the sha-1 hash + signature.set(hashBytes.subarray(0, 4), 1); + + // Now parse the modulus + var modLength = this._bytesToInt(keyBytes, 0); + var modulus = keyBytes.subarray(4, 4 + modLength); + + // Now parse the exponent + var expLength = this._bytesToInt(keyBytes, 4 + modLength); + var exponent = keyBytes.subarray(8 + modLength, 8 + modLength + expLength); + + // Ready to encrypt! + var pubkey = [modulus, exponent]; + var clearBytes = asmCrypto.string_to_bytes(email + '\0' + password); + var encryptedBytes = asmCrypto.RSA_OAEP_SHA1.encrypt(clearBytes, pubkey); + signature.set(encryptedBytes, 5); + + // Final url-safe encode in base64 and we're done + return this._arrayBufferToBase64(buffer); + }, + + _bytesToInt: function (byteArray, start) { + return (0xFF & byteArray[start]) << 24 | (0xFF & byteArray[(start + 1)]) << 16 + | (0xFF & byteArray[(start + 2)]) << 8 | 0xFF & byteArray[(start + 3)] + } +}); + +Tomahawk.resolver.instance = GMusicResolver; + +var gmusicCollection = Tomahawk.extend(Tomahawk.Collection, { + resolver: GMusicResolver, + settings: { + id: "gmusic", + prettyname: "Google Music", + description: GMusicResolver._email, + iconfile: "contents/images/icon.png", + trackcount: GMusicResolver.trackCount + } +}); diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/code/play-logo.png b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/play-logo.png new file mode 100644 index 000000000..0d97016a5 Binary files /dev/null and b/app/src/main/assets/js/resolvers/gmusic/content/contents/code/play-logo.png differ diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/icon.png new file mode 100644 index 000000000..835be0df0 Binary files /dev/null and b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconBackground.png new file mode 100644 index 000000000..07b3ec6e2 Binary files /dev/null and b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconWhite.png new file mode 100644 index 000000000..b9442d69b Binary files /dev/null and b/app/src/main/assets/js/resolvers/gmusic/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/gmusic/content/metadata.json b/app/src/main/assets/js/resolvers/gmusic/content/metadata.json new file mode 100644 index 000000000..b08224d59 --- /dev/null +++ b/app/src/main/assets/js/resolvers/gmusic/content/metadata.json @@ -0,0 +1,24 @@ +{ + "name": "Google Play Music", + "pluginName": "gmusic", + "author": "Sam Hanes, Lalit Maganti, Enno Gottschalk", + "email": "sam@maltera.com, lalitmaganti@gmail.com, mrmaffen@gmail.com", + "version": "0.6.6", + "website": "http://gettomahawk.com", + "description": "Streams music from your Google Play Music locker and the entire Google Music catalog for subscribers.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/gmusic.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/config.ui", + "contents/code/play-logo.png" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/code/hatchet-metadata.js b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/code/hatchet-metadata.js new file mode 100644 index 000000000..74ba3d344 --- /dev/null +++ b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/code/hatchet-metadata.js @@ -0,0 +1,242 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var HatchetMetadataResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Hatchet Metadata', + icon: 'hatchet-metadata.png', + weight: 0, // We cannot resolve, so use minimum weight + timeout: 15 + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + switch (type) { + case Tomahawk.UrlType.Album: + return /^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n]+\/[^\/\n]+$/.test(url); + case Tomahawk.UrlType.Artist: + return /^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n][^\/\n_]+$/.test(url); + case Tomahawk.UrlType.Track: + return /^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n]+\/_\/[^\/\n]+$/.test(url); + case Tomahawk.UrlType.Playlist: + return /^https?:\/\/(www\.)?hatchet\.is\/people\/[^\/\n]+\/playlists\/[^\/\n]+$/.test(url); + default: + return false; + } + }, + + lookupUrl: function (params) { + var url = params.url; + + Tomahawk.log("lookupUrl: " + url); + var urlParts = + url.split('/').filter(function (item) { + return item.length != 0; + }).map(function (s) { + return decodeURIComponent(s.replace(/\+/g, '%20')); + }); + if (/^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n]+\/[^\/\n]+$/.test(url)) { + Tomahawk.log("Found an album"); + // We have to deal with an Album + return { + type: Tomahawk.UrlType.Album, + artist: urlParts[urlParts.length - 2], + album: urlParts[urlParts.length - 1] + }; + } else if (/^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n][^\/\n_]+$/.test(url)) { + Tomahawk.log("Found an artist"); + // We have to deal with an Artist + return { + type: Tomahawk.UrlType.Artist, + artist: urlParts[urlParts.length - 1] + }; + } else if (/^https?:\/\/(www\.)?hatchet\.is\/music\/[^\/\n]+\/[^\/\n]+\/[^\/\n]+$/.test(url)) { + Tomahawk.log("Found a track"); + // We have to deal with a Track + return { + type: Tomahawk.UrlType.Track, + artist: urlParts[urlParts.length - 3], + track: urlParts[urlParts.length - 1] + }; + } else if (/^https?:\/\/(www\.)?hatchet\.is\/people\/[^\/\n]+\/playlists\/[^\/\n]+$/.test(url)) { + Tomahawk.log("Found a playlist"); + // We have to deal with a Playlist + var match = url.match(/^https?:\/\/(?:www\.)?hatchet\.is\/people\/[^\/\n]+\/playlists\/([^\/\n]+)$/); + var query = "https://api.hatchet.is/v2/playlists"; + var settings = { + data: { + "ids[]": match[1] + } + }; + return Tomahawk.get(query, settings).then(function (res) { + var query = "https://api.hatchet.is" + res.playlists[0].links.playlistEntries; + return Tomahawk.get(query).then(function (res) { + var entriesMap = {}; + res.playlistEntries.forEach(function (item) { + entriesMap[item.id] = item; + }); + var artistsMap = {}; + res.artists.forEach(function (item) { + artistsMap[item.id] = item; + }); + var tracksMap = {}; + res.tracks.forEach(function (item) { + tracksMap[item.id] = item; + }); + var tracks = res.playlists[0].playlistEntries.map(function (item) { + var track = tracksMap[entriesMap[item].track]; + return { + type: Tomahawk.UrlType.Track, + track: track.name, + artist: artistsMap[track.artist].name + }; + }); + Tomahawk.log("Reported found playlist '" + res.playlists[0].title + + "' containing " + tracks.length + " tracks"); + return { + type: Tomahawk.UrlType.Playlist, + title: res.playlists[0].title, + guid: res.playlists[0].id, + info: "A playlist on Hatchet.", + creator: res.playlists[0].user, + linkUrl: url, + tracks: tracks + }; + }); + }); + } + } +}); + +Tomahawk.resolver.instance = HatchetMetadataResolver; + +Tomahawk.PluginManager.registerPlugin('chartsProvider', { + + _baseUrl: "https://api.hatchet.is/v2/charts", + + countryCodes: { + defaultCode: "global", + codes: [ + {"Global": "global"} + ] + }, + + types: [ + {"Songs": "track"}, + {"Artists": "artist"}, + {"Albums": "album"} + ], + + /** + * Get the charts from the server specified by the given params map and parse them into the + * correct result format. + * + * @param params A map containing all of the necessary parameters describing the charts which to + * get from the server. + * + * Example: + * { countryCode: "us", //country code from the countryCodes map + * type: "regional" } //type from the types map + * + * @returns A map consisting of the contentType and parsed results. + * + * Example: + * { contentType: Tomahawk.UrlType.Track, + * results: [ + * { track: "We will rock you", + * artist: "Queen", + * album: "Greatest Hits" }, + * { track: "Bohemian rhapsody", + * artist: "Queen", + * album: "Greatest Hits" } + * ] + * } + * + */ + charts: function (params) { + var url = this._baseUrl; + var options = { + data: { + type: params.type + } + }; + return Tomahawk.get(url, options).then(function (response) { + var chartItemsMaps = {}; + response.chartItems.forEach(function (item) { + chartItemsMaps[item.id] = item; + }); + var tracksMaps = {}; + if (response.tracks) { + response.tracks.forEach(function (item) { + tracksMaps[item.id] = item; + }); + } + var artistsMaps = {}; + response.artists.forEach(function (item) { + artistsMaps[item.id] = item; + }); + var albumsMaps = {}; + if (response.albums) { + response.albums.forEach(function (item) { + albumsMaps[item.id] = item; + }); + } + var parsedResults = []; + for (var i = 0; i < response.chart[0].chartItems.length; i++) { + var chartItemId = response.chart[0].chartItems[i]; + var chartItem = chartItemsMaps[chartItemId]; + if (params.type == "track") { + var track = tracksMaps[chartItem.track]; + parsedResults.push({ + track: track.name, + artist: artistsMaps[track.artist].name, + album: "" + }); + } else if (params.type == "artist") { + parsedResults.push({ + artist: artistsMaps[chartItem.artist].name + }); + } else if (params.type == "album") { + var album = albumsMaps[chartItem.album]; + parsedResults.push({ + artist: artistsMaps[album.artist].name, + album: album.name + }); + } + } + var contentType; + if (params.type == "track") { + contentType = Tomahawk.UrlType.Track; + } else if (params.type == "artist") { + contentType = Tomahawk.UrlType.Artist; + } else if (params.type == "album") { + contentType = Tomahawk.UrlType.Album; + } + return { + contentType: contentType, + results: parsedResults + }; + }); + } + +}); diff --git a/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/icon.png new file mode 100644 index 000000000..0653d833d Binary files /dev/null and b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconBackground.png new file mode 100644 index 000000000..dcac20605 Binary files /dev/null and b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconWhite.png new file mode 100644 index 000000000..53fa67161 Binary files /dev/null and b/app/src/main/assets/js/resolvers/hatchet-metadata/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/hatchet-metadata/content/metadata.json b/app/src/main/assets/js/resolvers/hatchet-metadata/content/metadata.json new file mode 100644 index 000000000..5f1e2c574 --- /dev/null +++ b/app/src/main/assets/js/resolvers/hatchet-metadata/content/metadata.json @@ -0,0 +1,17 @@ +{ + "name": "Hatchet", + "pluginName": "hatchet-metadata", + "author": "Enno Gottschalk", + "email": "mrmaffen@googlemail.com", + "version": "0.2.2", + "website": "http://gettomahawk.com", + "description": "Supports loading/drag'n'drop of hatchet.is URLs.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/hatchet-metadata.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png" + } +} diff --git a/app/src/main/assets/js/resolvers/itunes/content/contents/code/itunes.js b/app/src/main/assets/js/resolvers/itunes/content/contents/code/itunes.js new file mode 100644 index 000000000..5b0da2ff1 --- /dev/null +++ b/app/src/main/assets/js/resolvers/itunes/content/contents/code/itunes.js @@ -0,0 +1,269 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +Tomahawk.PluginManager.registerPlugin('chartsProvider', { + + _baseUrl: "https://itunes.apple.com/", + + countryCodes: { + defaultCode: "us", + codes: [ + {"United States": "us"}, + {"United Kingdom": "gb"}, + {"Andorra": "ad"}, + {"Antigua and Barbuda": "ag"}, + {"Argentina": "ar"}, + {"Armenia": "am"}, + {"Australia": "au"}, + {"Austria": "at"}, + {"Azerbaijan": "az"}, + {"Bahamas": "bs"}, + {"Bahrain": "bh"}, + {"Bangladesh": "bd"}, + {"Barbados": "bb"}, + {"Belarus": "by"}, + {"Belgium": "be"}, + {"Belize": "bz"}, + {"Bermuda": "bm"}, + {"Bolivia": "bo"}, + {"Bosnia and Herzegovina": "ba"}, + {"Botswana": "bw"}, + {"Brazil": "br"}, + {"Brunei Darussalam": "bn"}, + {"Bulgaria": "bg"}, + {"Burkina Faso": "bf"}, + {"Burundi": "bi"}, + {"Cambodia": "kh"}, + {"Cameroon": "cm"}, + {"Canada": "ca"}, + {"Cape Verde": "cv"}, + {"Cayman Islands": "ky"}, + {"Central African Republic": "cf"}, + {"Chile": "cl"}, + {"Colombia": "co"}, + {"Comoros": "km"}, + {"Costa Rica": "cr"}, + {"Cuba": "cu"}, + {"Cyprus": "cy"}, + {"Czech": "cz"}, + {"Côte d’Ivoire": "ci"}, + {"Democratic Republic of the Congo": "cd"}, + {"Denmark": "dk"}, + {"Djibouti": "dj"}, + {"Dominica": "dm"}, + {"Dominican Republic": "do"}, + {"Ecuador": "ec"}, + {"Egypt": "eg"}, + {"El Salvador": "sv"}, + {"Equatorial Guinea": "gq"}, + {"Eritrea": "er"}, + {"Estonia": "ee"}, + {"Ethiopia": "et"}, + {"Falkland Islands": "fk"}, + {"Faroe Islands": "fo"}, + {"Fiji": "fj"}, + {"Finland": "fi"}, + {"France": "fr"}, + {"French Polynesia": "pf"}, + {"Gabon": "ga"}, + {"Gambia": "gm"}, + {"Georgia": "ge"}, + {"Germany": "de"}, + {"Greece": "gr"}, + {"Greenland": "gl"}, + {"Grenada": "gd"}, + {"Guatemala": "gt"}, + {"Guinea": "gn"}, + {"Guinea-Bissau": "gw"}, + {"Haiti": "ht"}, + {"Honduras": "hn"}, + {"Hong Kong": "hk"}, + {"Hungary": "hu"}, + {"India": "in"}, + {"Indonesia": "id"}, + {"Iran": "ir"}, + {"Iraq": "iq"}, + {"Ireland": "ie"}, + {"Isle of Man": "im"}, + {"Israel": "il"}, + {"Italy": "it"}, + {"Japan": "jp"}, + {"Jordan": "jo"}, + {"Kazakhstan": "kz"}, + {"Kenya": "ke"}, + {"Kiribati": "ki"}, + {"Kyrgyzstan": "kg"}, + {"Lao People’s Democratic Republic": "la"}, + {"Latvia": "lv"}, + {"Lebanon": "lb"}, + {"Lesotho": "ls"}, + {"Libyan Jamahiriya": "ly"}, + {"Liechtenstein": "li"}, + {"Lithuania": "lt"}, + {"Luxembourg": "lu"}, + {"Macao": "mo"}, + {"Malaysia": "my"}, + {"Maldives": "mv"}, + {"Malta": "mt"}, + {"Mauritius": "mu"}, + {"Mexico": "mx"}, + {"Moldova": "md"}, + {"Monaco": "mc"}, + {"Mongolia": "mn"}, + {"Montenegro": "me"}, + {"Morocco": "ma"}, + {"Mozambique": "mz"}, + {"Myanmar": "mm"}, + {"Namibia": "na"}, + {"Nauru": "nr"}, + {"Nepal": "np"}, + {"Netherlands": "nl"}, + {"New Caledonia": "nc"}, + {"New Zealand": "nz"}, + {"Nicaragua": "ni"}, + {"Niger": "ne"}, + {"Nigeria": "ng"}, + {"North Korea": "kp"}, + {"Norway": "no"}, + {"Oman": "om"}, + {"Panama": "pa"}, + {"Papua New Guinea": "pg"}, + {"Paraguay": "py"}, + {"Peru": "pe"}, + {"Philippines": "ph"}, + {"Poland": "pl"}, + {"Portugal": "pt"}, + {"Qatar": "qa"}, + {"Romania": "ro"}, + {"Russian Federation": "ru"}, + {"Rwanda": "rw"}, + {"Saint Helena": "sh"}, + {"Samoa": "ws"}, + {"Saudi Arabia": "sa"}, + {"Serbia and Montenegro": "yu"}, + {"Serbia": "rs"}, + {"Singapore": "sg"}, + {"Slovakia": "sk"}, + {"Slovenia": "si"}, + {"Somalia": "so"}, + {"South Africa": "za"}, + {"South Georgia and South Sandwich Islands": "gs"}, + {"Spain": "es"}, + {"Sri Lanka": "lk"}, + {"Sudan": "sd"}, + {"Swaziland": "sz"}, + {"Sweden": "se"}, + {"Switzerland": "ch"}, + {"Syrian Arab Republic": "sy"}, + {"Taiwan": "tw"}, + {"Tajikistan": "tj"}, + {"Thailand": "th"}, + {"Timor-Leste": "tl"}, + {"Togo": "tg"}, + {"Tonga": "to"}, + {"Trinidad and Tobago": "tt"}, + {"Turkey": "tr"}, + {"Turkmenistan": "tm"}, + {"Tuvalu": "tv"}, + {"Uganda": "ug"}, + {"Ukraine": "ua"}, + {"United Arab Emirates": "ae"}, + {"Uzbekistan": "uz"}, + {"Vanuatu": "vu"}, + {"Vatican": "va"}, + {"Venezuela": "ve"}, + {"Viet Nam": "vn"}, + {"Zambia": "zm"}, + {"Zimbabwe": "zw"} + ] + }, + + types: [ + {"Songs": "topsongs"}, + {"Albums": "topalbums"} + ], + + /** + * Get the charts from the server specified by the given params map and parse them into the + * correct result format. + * + * @param params A map containing all of the necessary parameters describing the charts which to + * get from the server. + * + * Example: + * { countryCode: "us", //country code from the countryCodes map + * type: "topsongs" } //type from the types map + * + * @returns A map consisting of the contentType and parsed results. + * + * Example: + * { contentType: Tomahawk.UrlType.Track, + * results: [ + * { track: "We will rock you", + * artist: "Queen", + * album: "Greatest Hits" }, + * { track: "Bohemian rhapsody", + * artist: "Queen", + * album: "Greatest Hits" } + * ] + * } + * + */ + charts: function (params) { + var url = this._baseUrl + params.countryCode + "/rss/" + params.type + + "/limit=100/explicit=true/json"; + return Tomahawk.get(url).then(function (response) { + var results = JSON.parse(response); + var firstITunesContentType = + results.feed.entry[0]["im:contentType"]["im:contentType"].attributes.term; + var contentType; + if (firstITunesContentType == "Track") { + contentType = Tomahawk.UrlType.Track; + } else if (firstITunesContentType == "Album") { + contentType = Tomahawk.UrlType.Album; + } else { + throw new Error("Unsupported contentType!"); + } + var parsedResults = []; + for (var i = 0; i < results.feed.entry.length; i++) { + var entry = results.feed.entry[i]; + var iTunesContentType = entry["im:contentType"]["im:contentType"].attributes.term; + if (iTunesContentType != firstITunesContentType) { + throw new Error("Two different contentTypes in one chart!"); + } + if (contentType == Tomahawk.UrlType.Track) { + parsedResults.push({ + track: entry["im:name"].label, + artist: entry["im:artist"].label, + album: entry["im:collection"]["im:name"].label + }); + } else if (contentType == Tomahawk.UrlType.Album) { + parsedResults.push({ + album: entry["im:name"].label, + artist: entry["im:artist"].label + }); + } + } + return { + contentType: contentType, + results: parsedResults + }; + }); + } + +}); \ No newline at end of file diff --git a/app/src/main/assets/js/resolvers/itunes/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/itunes/content/contents/images/icon.png new file mode 100644 index 000000000..ac3fd66d5 Binary files /dev/null and b/app/src/main/assets/js/resolvers/itunes/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconBackground.png new file mode 100644 index 000000000..fe2bcaf7d Binary files /dev/null and b/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconWhite.png new file mode 100644 index 000000000..559060254 Binary files /dev/null and b/app/src/main/assets/js/resolvers/itunes/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/itunes/content/metadata.json b/app/src/main/assets/js/resolvers/itunes/content/metadata.json new file mode 100644 index 000000000..85c5e4d8f --- /dev/null +++ b/app/src/main/assets/js/resolvers/itunes/content/metadata.json @@ -0,0 +1,17 @@ +{ + "name": "iTunes", + "pluginName": "itunes", + "author": "Enno", + "email": "mrmaffen@googlemail.com", + "version": "0.0.1", + "website": "https://itunes.apple.com", + "description": "Gets the latest and greatest iTunes Charts.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/itunes.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png" + } +} diff --git a/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo-icon.png b/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo-icon.png new file mode 100644 index 000000000..ebcef4e15 Binary files /dev/null and b/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo-icon.png differ diff --git a/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo.js b/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo.js new file mode 100644 index 000000000..a619f3098 --- /dev/null +++ b/app/src/main/assets/js/resolvers/jamendo/content/contents/code/jamendo.js @@ -0,0 +1,82 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2011, lasconic + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var JamendoResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Jamendo', + icon: 'jamendo-icon.png', + weight: 75, + timeout: 5 + }, + + _baseUrl: "https://api.jamendo.com/v3.0/tracks/", + + _clientId: "f52d7f12", + + _convertTracks: function (results) { + var tracks = []; + for (var i = 0; i < results.length; i++) { + var result = results[i]; + tracks.push({ + artist: result.artist_name, + album: result.album_name, + track: result.name, + source: this.settings.name, + url: decodeURI(result.audio), + duration: result.duration + }); + } + return tracks; + }, + + _searchRequest: function (query, limit) { + var that = this; + + var settings = { + data: { + client_id: this._clientId, + format: "json", + limit: limit, + search: query + } + }; + return Tomahawk.get(this._baseUrl, settings).then(function (response) { + return that._convertTracks(response.results); + }); + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + return this._searchRequest(artist + " " + track, 5); + }, + + search: function (params) { + var query = params.query; + + return this._searchRequest(query, 20); + } +}); + +Tomahawk.resolver.instance = JamendoResolver; diff --git a/app/src/main/assets/js/resolvers/jamendo/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/icon.png new file mode 100644 index 000000000..ebcef4e15 Binary files /dev/null and b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconBackground.png new file mode 100644 index 000000000..863a127d5 Binary files /dev/null and b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconWhite.png new file mode 100644 index 000000000..8cec0e093 Binary files /dev/null and b/app/src/main/assets/js/resolvers/jamendo/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/jamendo/content/metadata.json b/app/src/main/assets/js/resolvers/jamendo/content/metadata.json new file mode 100644 index 000000000..046680b49 --- /dev/null +++ b/app/src/main/assets/js/resolvers/jamendo/content/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Jamendo", + "pluginName": "jamendo", + "author": "lasconic and Enno", + "email": "lasconic@gmail.com", + "version": "0.2.1", + "website": "http://gettomahawk.com", + "description": "Searches Jamendo's free music database.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/jamendo.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/jamendo-icon.png" + ] + } +} diff --git a/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm-icon.png b/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm-icon.png new file mode 100644 index 000000000..98645d560 Binary files /dev/null and b/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm-icon.png differ diff --git a/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm.js b/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm.js new file mode 100644 index 000000000..10f28374c --- /dev/null +++ b/app/src/main/assets/js/resolvers/officialfm/content/contents/code/officialfm.js @@ -0,0 +1,85 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2011, lasconic + * Copyright 2011, Leo Franchi + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var OfficialfmResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Official.fm', + icon: 'officialfm-icon.png', + weight: 70, + timeout: 5 + }, + + _baseUrl: "http://api.official.fm/tracks/search", + + _apiKey: "lcghXySUP3nmYYpOALbPUJ6g30V1Z5hl", + + _convertTracks: function (results, limit) { + var tracks = []; + limit = limit || 9999; + for (var i = 0; i < results.length && i < limit; i++) { + var result = results[i].track; + if (result.streaming && result.streaming.http) { + tracks.push({ + artist: result.artist, + track: result.title, + source: this.settings.name, + url: result.streaming.http, + duration: result.duration + }); + } + } + return tracks; + }, + + _searchRequest: function (query, limit) { + var that = this; + + var settings = { + data: { + api_key: this._apiKey, + fields: "streaming", + api_version: "2.0", + q: query + } + }; + return Tomahawk.get(this._baseUrl, settings).then(function (response) { + return that._convertTracks(response.tracks, limit); + }); + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + return this._searchRequest(artist + " " + track, 20); + }, + + search: function (params) { + var query = params.query; + + return this._searchRequest(query); + } +}); + +Tomahawk.resolver.instance = OfficialfmResolver; diff --git a/app/src/main/assets/js/resolvers/officialfm/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/icon.png new file mode 100644 index 000000000..025b52c67 Binary files /dev/null and b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconBackground.png new file mode 100644 index 000000000..22b958a24 Binary files /dev/null and b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconWhite.png new file mode 100644 index 000000000..defc39a4b Binary files /dev/null and b/app/src/main/assets/js/resolvers/officialfm/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/officialfm/content/metadata.json b/app/src/main/assets/js/resolvers/officialfm/content/metadata.json new file mode 100644 index 000000000..23b44163c --- /dev/null +++ b/app/src/main/assets/js/resolvers/officialfm/content/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Official.fm", + "pluginName": "officialfm", + "author": "Leo, lasconic and Enno", + "email": "lasconic@gmail.com", + "version": "1.1.0", + "website": "http://gettomahawk.com", + "description": "Searches Official.fm for playable tracks.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/officialfm.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/officialfm-icon.png" + ] + } +} diff --git a/assets/js/soundcloud/config.ui b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/config.ui similarity index 100% rename from assets/js/soundcloud/config.ui rename to app/src/main/assets/js/resolvers/soundcloud/content/contents/code/config.ui diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud-icon.png b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud-icon.png new file mode 100644 index 000000000..35933bb85 Binary files /dev/null and b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud-icon.png differ diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.js b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.js new file mode 100644 index 000000000..ce5df92ec --- /dev/null +++ b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.js @@ -0,0 +1,347 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Thierry Göckel + * Copyright 2013, Uwe L. Korn + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var SoundcloudResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + soundcloudClientId: "TiNg2DRYhBnp01DA3zNag", + + echonestClientId: "JRIHWEP6GPOER2QQ6", + + baseUrl: "https://api.soundcloud.com/", + + settings: { + name: 'SoundCloud', + icon: 'soundcloud-icon.png', + weight: 85, + timeout: 15 + }, + + getConfigUi: function () { + var uiData = Tomahawk.readBase64("config.ui"); + return { + "widget": uiData, + fields: [ + { + name: "includeCovers", + widget: "covers", + property: "checked" + }, + { + name: "includeRemixes", + widget: "remixes", + property: "checked" + }, + { + name: "includeLive", + widget: "live", + property: "checked" + } + ], + images: [ + { + "soundcloud.png": Tomahawk.readBase64("soundcloud.png") + } + ] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + id: "includeCovers", + type: "checkbox", + label: "Include cover versions" + }, + { + id: "includeRemixes", + type: "checkbox", + label: "Include remix versions" + }, + { + id: "includeLive", + type: "checkbox", + label: "Include live versions" + } + ], + + newConfigSaved: function (newConfig) { + this.includeCovers = newConfig.includeCovers; + this.includeRemixes = newConfig.includeRemixes; + this.includeLive = newConfig.includeLive; + }, + + /** + * Initialize the Soundcloud resolver. + */ + init: function () { + // Set userConfig here + var userConfig = this.getUserConfig(); + if (userConfig) { + this.includeCovers = userConfig.includeCovers; + this.includeRemixes = userConfig.includeRemixes; + this.includeLive = userConfig.includeLive; + } else { + this.includeCovers = false; + this.includeRemixes = false; + this.includeLive = false; + } + + Tomahawk.PluginManager.registerPlugin("linkParser", this); + }, + + _isValidTrack: function (trackTitle, origTitle) { + if (!this.includeCovers && + trackTitle.search(/cover/i) >= 0 && + origTitle.search(/cover/i) < 0) { + return false; + } + if (!this.includeRemixes && + trackTitle.search(/mix/i) >= 0 && + origTitle.search(/mix/i) < 0) { + return false; + } + if (!this.includeLive && + trackTitle.search(/live/i) >= 0 && + origTitle.search(/live/i) < 0) { + return false; + } + return true; + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + var that = this; + + var url = this.baseUrl + "tracks.json"; + var settings = { + data: { + consumer_key: this.soundcloudClientId, + filter: "streamable", + limit: 20, + q: [artist, track].join(" ") + } + }; + return Tomahawk.get(url, settings).then(function (response) { + var results = []; + for (var i = 0; i < response.length; i++) { + // Check if the title-string contains the track name we are looking for. Also check + // if the artist name can be found in either the title-string or the username. Last + // but not least we make sure that we only include covers/remixes and live versions + // if the user wants us to. + if (!response[i] || !response[i].title + || (response[i].title.toLowerCase().indexOf(artist.toLowerCase()) < 0 + && response[i].user.username.toLowerCase().indexOf(artist.toLowerCase()) < 0) + || response[i].title.toLowerCase().indexOf(track.toLowerCase()) < 0 + || !that._isValidTrack(response[i].title, track)) { + continue; + } + + var guessedMetaData = that._guessMetaData(response[i].title); + var title = guessedMetaData ? guessedMetaData.track : response[i].title; + + var result = { + track: title, + artist: artist, + bitrate: 128, + mimetype: "audio/mpeg", + source: that.settings.name, + duration: response[i].duration / 1000, + year: response[i].release_year, + url: response[i].stream_url + ".json?client_id=" + that.soundcloudClientId + }; + if (response[i].permalink_url) { + result.linkUrl = response[i].permalink_url; + } + results.push(result); + } + return results; + }); + }, + + _guessMetaData: function (title) { + var matches = title.match(/\s*(.+?)\s*(?:\s[-\u2014]|\s["']|:)\s*["']?(.+?)["']?\s*$/); + if (matches && matches.length > 2) { + return { + track: matches[2], + artist: matches[1] + }; + } + matches = title.match(/\s*(.+?)\s*[-\u2014]+\s*(.+?)\s*$/); + if (matches && matches.length > 2) { + return { + track: matches[2], + artist: matches[1] + }; + } + }, + + search: function (params) { + var query = params.query; + + var that = this; + + var url = this.baseUrl + "tracks.json"; + var settings = { + data: { + consumer_key: this.soundcloudClientId, + filter: "streamable", + limit: 50, + q: query.replace("'", "") + } + }; + return Tomahawk.get(url, settings).then(function (response) { + var results = []; + for (var i = 0; i < response.length; i++) { + // Make sure that we only include covers/remixes and live versions if the user wants + // us to. + if (!response[i] || !response[i].title + || !that._isValidTrack(response[i].title, "")) { + continue; + } + + var result = { + mimetype: "audio/mpeg", + bitrate: 128, + duration: response[i].duration / 1000, + year: response[i].release_year, + url: response[i].stream_url + ".json?client_id=" + that.soundcloudClientId + }; + if (response[i].permalink_url) { + result.linkUrl = response[i].permalink_url; + } + + var guessedMetaData = that._guessMetaData(response[i].title); + if (guessedMetaData) { + result.track = guessedMetaData.track; + result.artist = guessedMetaData.artist; + } else if (response[i].user.username) { + // We weren't able to guess the artist and track name, so we assume the username + // as the artist name. No further check with Echonest needed since it's very + // unlikely that the username actually is the name of the track and not of the + // artist. + result.track = response[i].title; + result.artist = response[i].user.username; + } + results.push(result); + } + return results; + }); + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + // Soundcloud only returns tracks and playlists + switch (type) { + case TomahawkUrlType.Album: + return false; + case TomahawkUrlType.Artist: + return false; + default: + return (/https?:\/\/(www\.)?soundcloud.com\//).test(url); + } + }, + + _convertTrack: function (track) { + var result = { + type: Tomahawk.UrlType.Track, + track: track.title, + artist: track.user.username + }; + + if (!(track.stream_url === null || typeof track.stream_url === "undefined")) { + result.hint = track.stream_url + "?client_id=" + this.soundcloudClientId; + } + return result; + }, + + lookupUrl: function (params) { + var url = params.url; + + var that = this; + + var queryUrl = this.baseUrl + "resolve.json"; + var settings = { + data: { + client_id: this.soundcloudClientId, + url: url.replace(/\/likes$/, '') + } + }; + return Tomahawk.get(queryUrl, settings).then(function (response) { + if (response.kind == "playlist") { + var result = { + type: Tomahawk.UrlType.Playlist, + title: response.title, + guid: 'soundcloud-playlist-' + response.id.toString(), + info: response.description, + creator: response.user.username, + linkUrl: response.permalink_url, + tracks: [] + }; + response.tracks.forEach(function (item) { + result.tracks.push(that._convertTrack(item)); + }); + return result; + } else if (response.kind == "track") { + return that._convertTrack(response); + } else if (response.kind == "user") { + var url2 = response.uri; + var prefix = 'soundcloud-'; + var title = response.full_name + "'s "; + if (url.indexOf("/likes") === -1) { + url2 += "/tracks.json?client_id=" + that.soundcloudClientId; + prefix += 'user-'; + title += "Tracks"; + } else { + url2 += "/favorites.json?client_id=" + that.soundcloudClientId; + prefix += 'favortites-'; + title += "Favorites"; + } + return Tomahawk.get(url2).then(function (response) { + var result = { + type: Tomahawk.UrlType.Playlist, + title: title, + guid: prefix + response.id.toString(), + info: title, + creator: response.username, + linkUrl: response.permalink_url, + tracks: [] + }; + response.forEach(function (item) { + result.tracks.push(that._convertTrack(item)); + }); + return result; + }); + } else { + Tomahawk.log("Could not parse SoundCloud URL: " + url); + throw new Error("Could not parse SoundCloud URL: " + url); + } + }); + } +}); + +Tomahawk.resolver.instance = SoundcloudResolver; diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.png b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.png new file mode 100644 index 000000000..0da9a23a2 Binary files /dev/null and b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/soundcloud.png differ diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/test.js b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/test.js new file mode 100644 index 000000000..0e80c9d74 --- /dev/null +++ b/app/src/main/assets/js/resolvers/soundcloud/content/contents/code/test.js @@ -0,0 +1,90 @@ +var buster = require("buster"); +var nock = require("nock"); +var utils = require("../../../../test/utils.js"); + +buster.testCase("soundcloud", { + setUp: function (done) { + utils.loadResolver('soundcloud', this, done); + }, + + "test capabilities": function () { + buster.assert(this.context.hasCapability('urllookup')); + buster.refute(this.context.hasCapability('playlistsync')); + buster.refute(this.context.hasCapability('browsable')); + }, + + "test resolving": function (done) { + this.context.on('track-result', function (qid, result) { + buster.assert.equals("qid-001", qid); + buster.assert.equals("Bloc Party", result.artist); + buster.assert.equals("Ratchet", result.track); + done(); + }); + // This is not the original reponse but one of the answer slightly adjusted + nock("https://api.soundcloud.com") + .get("/tracks.json?consumer_key=TiNg2DRYhBnp01DA3zNag&filter=streamable&q=Bloc%20Party+Ratchet") + .reply(200, JSON.stringify([ + { + "kind": "track", + "id": 112482070, + "created_at": "2013/09/25 21:51:32 +0000", + "user_id": 932254, + "duration": 1249970, + "commentable": true, + "state": "finished", + "original_content_size": 49995285, + "sharing": "public", + "tag_list": "kele okereke bloc party tapes k7 records dj mix cd download", + "permalink": "20-min-snippet", + "streamable": true, + "embeddable_by": "all", + "downloadable": false, + "purchase_url": null, + "label_id": null, + "purchase_title": null, + "genre": "Dance", + "title": "Bloc Party - Ratchet", + "description": "<--truncated-->", + "label_name": "", + "release": "", + "track_type": "recording", + "key_signature": "", + "isrc": "", + "video_url": null, + "bpm": null, + "release_year": null, + "release_month": null, + "release_day": null, + "original_format": "mp3", + "license": "all-rights-reserved", + "uri": "https://api.soundcloud.com/tracks/112482070", + "user": { + "id": 932254, + "kind": "user", + "permalink": "keleokereke", + "username": "Kele Okereke", + "uri": "https://api.soundcloud.com/users/932254", + "permalink_url": "http://soundcloud.com/keleokereke", + "avatar_url": "https://i1.sndcdn.com/avatars-000056100835-hnk1ji-large.jpg?30a2558" + }, + "permalink_url": "http://soundcloud.com/keleokereke/20-min-snippet", + "artwork_url": "https://i1.sndcdn.com/artworks-000059074336-nf414w-large.jpg?30a2558", + "waveform_url": "https://w1.sndcdn.com/nprcFygtaWnU_m.png", + "stream_url": "https://api.soundcloud.com/tracks/112482070/stream", + "playback_count": 38057, + "download_count": 0, + "favoritings_count": 679, + "comment_count": 39, + "attachments_uri": "https://api.soundcloud.com/tracks/112482070/attachments" + } + ])); + this.instance.resolve("qid-001", "Bloc Party", "", "Ratchet"); + }, + + "// test search": function () { + }, + + "// test url lookup": function () { + } +}); + diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/icon.png new file mode 100644 index 000000000..bffff0590 Binary files /dev/null and b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconBackground.png new file mode 100644 index 000000000..56cc31d05 Binary files /dev/null and b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconWhite.png new file mode 100644 index 000000000..16347383e Binary files /dev/null and b/app/src/main/assets/js/resolvers/soundcloud/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/soundcloud/content/metadata.json b/app/src/main/assets/js/resolvers/soundcloud/content/metadata.json new file mode 100644 index 000000000..11e21b743 --- /dev/null +++ b/app/src/main/assets/js/resolvers/soundcloud/content/metadata.json @@ -0,0 +1,22 @@ +{ + "name": "SoundCloud", + "pluginName": "soundcloud", + "author": "Thierry Göckel, Enno Gottschalk", + "email": "thierry@strayrayday.lu", + "version": "1.0.3", + "website": "http://gettomahawk.com", + "description": "Resolves to playable tracks on SoundCloud, including live, cover and remixed versions if specified.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/soundcloud.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/soundcloud.png", + "contents/code/soundcloud-icon.png", + "contents/code/config.ui" + ] + } +} diff --git a/app/src/main/assets/js/resolvers/spotify/LICENSE b/app/src/main/assets/js/resolvers/spotify/LICENSE new file mode 100644 index 000000000..1ef4329d1 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Uwe L. Korn + +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. diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/spotify/content/contents/code/config.ui new file mode 100644 index 000000000..a88293fcc --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/content/contents/code/config.ui @@ -0,0 +1,107 @@ + + + Form + + + + 0 + 0 + 1506 + 1566 + + + + Form + + + + + + + 16777215 + 250 + + + + + 6 + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + + 200 + 200 + + + + + 0 + 0 + + + + spotify.png + + + true + + + Qt::AlignCenter + + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Username: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Password: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.js b/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.js new file mode 100644 index 000000000..8ab3dea64 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.js @@ -0,0 +1,744 @@ +/* + * Copyright 2014, Uwe L. Korn + * Copyright 2015, Enno Gottschalk + * + * The MIT License (MIT) + * + * 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. + */ + +var SpotifyResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Spotify', + icon: 'spotify.png', + weight: 95, + timeout: 15 + }, + + _clientId: "q3r9p989687p496no2s92p9r84s779qp", + + _clientSecret: "789r9n607poo4s9no6998771s969o630", + + _tokenEndPoint: "https://accounts.spotify.com/api/token", + + _redirectUri: "tomahawkspotifyresolver://callback", + + _storageKeyRefreshToken: "spotify_refresh_token", + + /** + * Get the access token. Refresh when it is expired. + */ + getAccessToken: function () { + var that = this; + if (!this._getAccessTokenPromise || new Date().getTime() + 60000 > that._accessTokenExpires) { + Tomahawk.log("Access token is not valid. We need to get a new one."); + this._getAccessTokenPromise = new RSVP.Promise(function (resolve, reject) { + var refreshToken = Tomahawk.localStorage.getItem(that._storageKeyRefreshToken); + if (!refreshToken) { + Tomahawk.log("Can't fetch new access token, because there's no stored refresh " + + "token. Are you logged in?"); + reject("Can't fetch new access token, because there's no stored refresh" + + " token. Are you logged in?"); + } + resolve(refreshToken); + }).then(function (result) { + Tomahawk.log("Fetching new access token ..."); + var settings = { + headers: { + "Authorization": "Basic " + + Tomahawk.base64Encode(that._spell(that._clientId) + + ":" + that._spell(that._clientSecret)), + "Content-Type": "application/x-www-form-urlencoded" + }, + data: { + "grant_type": "refresh_token", + "refresh_token": result + } + }; + return Tomahawk.post(that._tokenEndPoint, settings) + .then(function (res) { + that._accessToken = res.access_token; + that._accessTokenExpires = new Date().getTime() + res.expires_in * 1000; + Tomahawk.log("Received new access token!"); + return { + accessToken: res.access_token + }; + }); + }); + } + return this._getAccessTokenPromise; + }, + + login: function () { + Tomahawk.log("Starting login"); + + var authUrl = "https://accounts.spotify.com/authorize"; + authUrl += "?client_id=" + this._spell(this._clientId); + authUrl += "&response_type=code"; + authUrl += "&redirect_uri=" + encodeURIComponent(this._redirectUri); + authUrl + += "&scope=playlist-read-private%20streaming%20user-read-private%20user-library-read"; + authUrl += "&show_dialog=true"; + + var that = this; + + var params = { + url: authUrl + }; + return Tomahawk.NativeScriptJobManager.invoke("showWebView", params).then( + function (result) { + var error = that._getParameterByName(result.url, "error"); + if (error) { + Tomahawk.log("Authorization failed: " + error); + return error; + } else { + Tomahawk.log("Authorization successful, fetching new refresh token ..."); + var settings = { + headers: { + "Authorization": "Basic " + + Tomahawk.base64Encode(that._spell(that._clientId) + + ":" + that._spell(that._clientSecret)), + "Content-Type": "application/x-www-form-urlencoded" + }, + data: { + grant_type: "authorization_code", + code: encodeURIComponent(that._getParameterByName(result.url, "code")), + redirect_uri: encodeURIComponent(that._redirectUri) + } + }; + + return Tomahawk.post(that._tokenEndPoint, settings) + .then(function (response) { + Tomahawk.localStorage.setItem(that._storageKeyRefreshToken, + response.refresh_token); + Tomahawk.log("Received new refresh token!"); + return TomahawkConfigTestResultType.Success; + }); + } + }); + }, + + logout: function () { + Tomahawk.localStorage.removeItem(this._storageKeyRefreshToken); + return TomahawkConfigTestResultType.Logout; + }, + + isLoggedIn: function () { + var refreshToken = Tomahawk.localStorage.getItem(this._storageKeyRefreshToken); + return refreshToken !== null && refreshToken.length > 0; + }, + + /** + * Returns the value of the query parameter with the given name from the given URL. + */ + _getParameterByName: function (url, name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(url); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + }, + + _spell: function (a) { + magic = function (b) { + return (b = (b) ? b : this).split("").map(function (d) { + if (!d.match(/[A-Za-z]/)) { + return d + } + c = d.charCodeAt(0) >= 96; + k = (d.toLowerCase().charCodeAt(0) - 96 + 12) % 26 + 1; + return String.fromCharCode(k + (c ? 96 : 64)) + }).join("") + }; + return magic(a) + }, + + init: function () { + Tomahawk.PluginManager.registerPlugin("linkParser", this); + }, + + getStreamUrl: function (params) { + var url = params.url; + + return { + url: url.replace("spotify://track/", "") + }; + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + var q = "artist:\"" + artist + "\" track:\"" + track + "\""; + if (album) { + q += " album:\"" + album + "\""; + } + + return this._search(q); + }, + + search: function (params) { + var query = params.query; + + return this._search(query); + }, + + _search: function (query) { + var that = this; + + return this.getAccessToken().then(function (result) { + var url = "https://api.spotify.com/v1/search"; + var settings = { + data: { + market: "from_token", + type: "track", + q: query + }, + headers: { + Authorization: "Bearer " + result.accessToken + } + }; + return Tomahawk.get(url, settings).then(function (response) { + return response.tracks.items.map(function (item) { + return { + artist: item.artists[0].name, + album: item.album.name, + duration: item.duration_ms / 1000, + source: that.settings.name, + track: item.name, + url: "spotify://track/" + item.id + }; + }); + }); + }); + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + if (!url) { + throw new Error("Provided url was empty or null!"); + } + switch (type) { + case TomahawkUrlType.Album: + return /spotify:album:([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/album\/([^\/\?]+)/.test(url); + case TomahawkUrlType.Artist: + return /spotify:artist:([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/artist\/([^\/\?]+)/.test(url); + case TomahawkUrlType.Playlist: + return /spotify:user:([^:]+):playlist:([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/user\/([^\/]+)\/playlist\/([^\/\?]+)/.test(url); + case TomahawkUrlType.Track: + return /spotify:track:([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/track\/([^\/\?]+)/.test(url); + // case TomahawkUrlType.Any: + default: + return /spotify:(album|artist|track):([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/(album|artist|track)\/([^\/\?]+)/.test(url) + || /spotify:user:([^:]+):playlist:([^:]+)/.test(url) + || /https?:\/\/(?:play|open)\.spotify\.[^\/]+\/user\/([^\/]+)\/playlist\/([^\/\?]+)/.test(url); + } + }, + + lookupUrl: function (params) { + var url = params.url; + Tomahawk.log("lookupUrl: " + url); + + var match = url.match(/spotify[/:]+(album|artist|track)[/:]+([^/:?]+)/); + if (match == null) { + match + = url.match(/https?:\/\/(?:play|open)\.spotify\.[^\/]+\/(album|artist|track)\/([^\/\?]+)/); + } + var playlistmatch = url.match(/spotify[/:]+user[/:]+([^/:]+)[/:]+playlist[/:]+([^/:?]+)/); + if (playlistmatch == null) { + playlistmatch + = url.match(/https?:\/\/(?:play|open)\.spotify\.[^\/]+\/user\/([^\/]+)\/playlist\/([^\/\?]+)/); + } + if (match != null) { + var query = 'https://ws.spotify.com/lookup/1/.json?uri=spotify:' + match[1] + ':' + + match[2]; + Tomahawk.log("Found album/artist/track, calling " + query); + return Tomahawk.get(query).then(function (response) { + if (match[1] == "artist") { + Tomahawk.log("Reported found artist '" + response.artist.name + "'"); + return { + type: Tomahawk.UrlType.Artist, + artist: response.artist.name + }; + } else if (match[1] == "album") { + Tomahawk.log("Reported found album '" + response.album.name + "' by '" + + response.album.artist + "'"); + return { + type: Tomahawk.UrlType.Album, + album: response.album.name, + artist: response.album.artist + }; + } else if (match[1] == "track") { + var artist = response.track.artists.map(function (item) { + return item.name; + }).join(" & "); + Tomahawk.log("Reported found track '" + response.track.name + "' by '" + artist + + "'"); + return { + type: Tomahawk.UrlType.Track, + track: response.track.name, + artist: artist + }; + } + }); + } else if (playlistmatch != null) { + var query = 'http://spotikea.tomahawk-player.org/browse/spotify:user:' + + playlistmatch[1] + ':playlist:' + playlistmatch[2]; + Tomahawk.log("Found playlist, calling url: '" + query + "'"); + return Tomahawk.get(query).then(function (res) { + var tracks = res.playlist.result.map(function (item) { + return { + type: Tomahawk.UrlType.Track, + track: item.title, + artist: item.artist + }; + }); + Tomahawk.log("Reported found playlist '" + res.playlist.name + "' containing " + + tracks.length + " tracks"); + return { + type: Tomahawk.UrlType.Playlist, + title: res.playlist.name, + guid: "spotify-playlist-" + url, + info: "A playlist on Spotify.", + creator: res.playlist.creator, + linkUrl: url, + tracks: tracks + }; + }); + } + } +}); + +Tomahawk.resolver.instance = SpotifyResolver; + +Tomahawk.PluginManager.registerPlugin('chartsProvider', { + + _baseUrl: "https://spotifycharts.com/", + + countryCodes: { + defaultCode: "global", + codes: [ + {"Global": "global"}, + {"United States": "us"}, + {"United Kingdom": "gb"}, + {"Andorra": "ad"}, + {"Argentina": "ar"}, + {"Australia": "au"}, + {"Austria": "at"}, + {"Belgium": "be"}, + {"Bolivia": "bo"}, + {"Brazil": "br"}, + {"Bulgaria": "bg"}, + {"Canada": "ca"}, + {"Chile": "cl"}, + {"Colombia": "co"}, + {"Costa Rica": "cr"}, + {"Cyprus": "cy"}, + {"Czech Republic": "cz"}, + {"Denmark": "dk"}, + {"Dominican Republic": "do"}, + {"Ecuador": "ec"}, + {"El Salvador": "sv"}, + {"Estonia": "ee"}, + {"Finland": "fi"}, + {"France": "fr"}, + {"Germany": "de"}, + {"Greece": "gr"}, + {"Guatemala": "gt"}, + {"Honduras": "hn"}, + {"Hong Kong": "hk"}, + {"Hungary": "hu"}, + {"Iceland": "is"}, + {"Ireland": "ie"}, + {"Italy": "it"}, + {"Latvia": "lv"}, + {"Lithuania": "lt"}, + {"Luxembourg": "lu"}, + {"Malaysia": "my"}, + {"Malta": "mt"}, + {"Mexico": "mx"}, + {"Netherlands": "nl"}, + {"New Zealand": "nz"}, + {"Nicaragua": "ni"}, + {"Norway": "no"}, + {"Panama": "pa"}, + {"Paraguay": "py"}, + {"Peru": "pe"}, + {"Philippines": "ph"}, + {"Poland": "pl"}, + {"Portugal": "pt"}, + {"Singapore": "sg"}, + {"Slovakia": "sk"}, + {"Spain": "es"}, + {"Sweden": "se"}, + {"Switzerland": "ch"}, + {"Taiwan": "tw"}, + {"Turkey": "tr"}, + {"Uruguay": "uy"} + ] + }, + + types: [ + {"Top 200": "regional"}, + {"Viral 50": "viral"} + ], + + /** + * Get the charts from the server specified by the given params map and parse them into the + * correct result format. + * + * @param params A map containing all of the necessary parameters describing the charts which to + * get from the server. + * + * Example: + * { countryCode: "us", //country code from the countryCodes map + * type: "regional" } //type from the types map + * + * @returns A map consisting of the contentType and parsed results. + * + * Example: + * { contentType: Tomahawk.UrlType.Track, + * results: [ + * { track: "We will rock you", + * artist: "Queen", + * album: "Greatest Hits" }, + * { track: "Bohemian rhapsody", + * artist: "Queen", + * album: "Greatest Hits" } + * ] + * } + * + */ + charts: function (params) { + var url = this._baseUrl + params.type + "/" + params.countryCode + "/daily/latest/download"; + return Tomahawk.get(url).then(function (response) { + var rows = response.split("\n"); + var parsedResults = []; + for (var i = 1; i < rows.length; i++) { + if (rows[i]) { + var columns = rows[i].split(","); + if (columns && columns.length > 2) { + parsedResults.push({ + track: columns[1].replace(/(^")|("$)/g, ""), + artist: columns[2].replace(/(^")|("$)/g, ""), + album: "" + }); + } + } + } + return { + contentType: Tomahawk.UrlType.Track, + results: parsedResults + }; + }); + } + +}); + +Tomahawk.PluginManager.registerPlugin('playlistGenerator', { + + _clientCredsTokenExpires: 0, + + _sessions: {}, + + /** + * If the user is logged into Spotify, this function returns the already available access token. + * Otherwise it fetches an access token through the Client Credentials auth flow. + */ + _getAccessToken: function () { + var that = this; + return SpotifyResolver.getAccessToken().then(function (accessToken) { + // User is logged into Spotify. We'll use his accessToken + return accessToken; + }, function (error) { + // User is not logged into Spotify. + // We need to get a basic accessToken through the client credentials auth flow + if (!that._getAccessTokenPromise + || new Date().getTime() + 60000 > that._clientCredsTokenExpires) { + Tomahawk.log("ClientCreds access token is not valid. We need to get a new one."); + Tomahawk.log("Fetching new ClientCreds access token ..."); + var options = { + headers: { + "Authorization": "Basic " + + Tomahawk.base64Encode(SpotifyResolver._spell(SpotifyResolver._clientId) + + ":" + SpotifyResolver._spell(SpotifyResolver._clientSecret)), + "Content-Type": "application/x-www-form-urlencoded" + }, + data: { + grant_type: "client_credentials" + } + }; + that._getAccessTokenPromise = + Tomahawk.post(SpotifyResolver._tokenEndPoint, options).then(function (res) { + that._clientCredsTokenExpires = new Date().getTime() + res.expires_in * 1000; + Tomahawk.log("Received new ClientCreds access token!"); + return { + accessToken: res.access_token + }; + }); + } + return that._getAccessTokenPromise; + }); + }, + + /** + * Fetch all available genres from the Spotify API + */ + _genres: function () { + var that = this; + if (!that._genrePromise) { + that._genrePromise = that._getAccessToken().then(function (result) { + var url = "https://api.spotify.com/v1/recommendations/available-genre-seeds"; + var settings = { + headers: { + Authorization: "Bearer " + result.accessToken + } + }; + return Tomahawk.get(url, settings).then(function (response) { + return response.genres; + }); + }); + } + return that._genrePromise; + }, + + /** + * Searches the source for available playlist seeds like artists or songs. The results from this + * function are later being used as a seed to fill the playlist with tracks. + * + * @param params Example: { query: "Queen rock you" } + * + * @returns Example: { artists: [ { artist: 'Queen', id: '123' }, + * { artist: 'Queens', id: '124' } ], + * albums: [ { artist: 'Queen', album: 'Greatest Hits', id: '125' } ], + * tracks: [ { artist: 'Queen', track: 'We will rock you', id: '126' } ], + * genres: [ { name: 'Rock' }, + * { name: 'Alternative Rock' } ], + * moods: [ { name: 'Happy' } ] } + */ + search: function (params) { + var that = this; + return that._getAccessToken().then(function (result) { + var promises = []; + var url = "https://api.spotify.com/v1/search"; + var settings = { + data: { + type: "track,artist", + q: params.query + }, + headers: { + Authorization: "Bearer " + result.accessToken + } + }; + promises.push(Tomahawk.get(url, settings).then(function (response) { + return { + artists: response.artists.items.map(function (item) { + return { + artist: item.name, + id: item.id + }; + }), + tracks: response.tracks.items.map(function (item) { + return { + artist: item.artists[0].name, + album: item.album.name, + track: item.name, + id: item.id + }; + }) + }; + })); + promises.push(that._genres().then(function (allGenres) { + // Search for genres by manually iterating through all available genres + return { + genres: allGenres.filter(function (item) { + return item.toLowerCase().indexOf(params.query.toLowerCase()) > -1; + }).map(function (item) { + return { + name: item + }; + }) + }; + })); + return RSVP.all(promises).then(function (results) { + return { + artists: results[0].artists, + albums: [], + tracks: results[0].tracks, + genres: results[1].genres, + moods: [] + }; + }); + }); + }, + + /** + * Converts the given params to the format the Spotify API expects. If an artist or track + * doesn't yet have an id, fetch the id automagically. + */ + _buildSettingsData: function (params) { + var artistIds = []; + var trackIds = []; + var genreIds = []; + var promises = []; + if (params.artists) { + for (var i = 0; i < params.artists.length; i++) { + var artist = params.artists[i]; + if (artist.id) { + artistIds.push(artist.id); + } else if (artist.artist) { + // No artist id provided, so we have to search for it + var queryParams = { + query: artist.artist + }; + promises.push(this.search(queryParams).then(function (result) { + if (result.artists && result.artists.length > 0) { + // Let's use the first result + Tomahawk.log("Resolved artist to id: " + result.artists[0].id); + artistIds.push(result.artists[0].id); + } + })); + } + } + } else if (params.tracks) { + for (var i = 0; i < params.tracks.length; i++) { + var track = params.tracks[i]; + if (track.id) { + trackIds.push(track.id); + } else if (track.track && track.artist) { + // No artist id provided, so we have to search for it + var queryParams = { + query: "track:" + track.track + " artist:" + track.artist + }; + promises.push(this.search(queryParams).then(function (result) { + if (result.tracks && result.tracks.length > 0) { + // Let's use the first result + Tomahawk.log("Resolved track to id: " + result.tracks[0].id); + trackIds.push(result.tracks[0].id); + } + })); + } + } + } else if (params.genres) { + genreIds = params.genres.map(function (item) { + return item.name; + }); + } + return RSVP.all(promises).then(function () { + var result = { + limit: 100 + }; + if (artistIds.length > 0) { + result.seed_artists = artistIds.join(','); + } + if (trackIds.length > 0) { + result.seed_tracks = trackIds.join(','); + } + if (genreIds.length > 0) { + result.seed_genres = genreIds.join(','); + } + return result; + }); + }, + + /** + * This function requests a new set of tracks from the Spotify API based on the + * artists/tracks/genres seeds that are given in the params object. + * + * @param params + * Using params from a previous session: + * Example: { sessionId: "12476294" } + * + * Using params to create a new session: + * Example: { artists: [ { artist: 'Queen', id: '123' }, + * { artist: 'Queens', id: '124' } ], + * tracks: [ { artist: 'Queen', track: 'We will rock you', id: '126' } ], + * genres: [ { name: 'Rock' }, + * { name: 'Alternative Rock' } ] } + * + * @returns Example: { sessionId: "124252622", // this id should be used in subsequent calls + * results: [ + * { artist: 'Queen', + * track: 'We will rock you', + * album: 'Greatest Hits' } + * { artist: 'Queen', + * track: 'We won't rock you', + * album: 'Crappiest Hits' } + * ] + * } + */ + fillPlaylist: function (params) { + var that = this; + return that._getAccessToken().then(function (result) { + var url = "https://api.spotify.com/v1/recommendations"; + var settings = { + headers: { + Authorization: "Bearer " + result.accessToken + } + }; + var settingsDataPromise; + if (params.sessionId && that._sessions[params.sessionId]) { + // We can use the cached settingsData from the previous call + settingsDataPromise = RSVP.resolve(that._sessions[params.sessionId]); + } else { + // No cached settingsData available + settingsDataPromise = that._buildSettingsData(params); + } + return settingsDataPromise.then(function (settingsData) { + var sessionId = params.sessionId; + if (!sessionId) { + sessionId = new Date().getTime(); + } + // Cache the settingsData + that._sessions[sessionId] = settingsData; + + settings.data = settingsData; + return Tomahawk.get(url, settings).then(function (response) { + var results = []; + if (response.tracks) { + results = response.tracks.map(function (item) { + return { + artist: item.artists[0].name, + album: item.album.name, + track: item.name + }; + }); + } + Tomahawk.log("Filled playlist with sessionId: " + sessionId + + ", resultCount: " + results.length); + return { + sessionId: sessionId, + results: results + }; + }); + }); + }); + } + +}); + diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.png b/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.png new file mode 100644 index 000000000..d426d24ac Binary files /dev/null and b/app/src/main/assets/js/resolvers/spotify/content/contents/code/spotify.png differ diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/spotify/content/contents/images/icon.png new file mode 100644 index 000000000..d426d24ac Binary files /dev/null and b/app/src/main/assets/js/resolvers/spotify/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconBackground.png new file mode 100644 index 000000000..cd6f617af Binary files /dev/null and b/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconWhite.png new file mode 100644 index 000000000..16e0089d0 Binary files /dev/null and b/app/src/main/assets/js/resolvers/spotify/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/spotify/content/metadata.json b/app/src/main/assets/js/resolvers/spotify/content/metadata.json new file mode 100644 index 000000000..b5da2e0e7 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/content/metadata.json @@ -0,0 +1,24 @@ +{ + "name": "Spotify", + "pluginName": "spotify", + "author": "Uwe L. Korn, Enno Gottschalk", + "email": "uwelk@xhochy.com", + "version": "0.4.1", + "website": "http://gettomahawk.com", + "description": "Stream music from Spotify. Requires a Premium account.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/spotify.js", + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "scripts": [], + "resources": [ + "contents/code/config.ui", + "contents/code/spotify.png" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/spotify/native/CMakeLists.txt b/app/src/main/assets/js/resolvers/spotify/native/CMakeLists.txt new file mode 100644 index 000000000..b0a0076cc --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/native/CMakeLists.txt @@ -0,0 +1,41 @@ +project( spotify-native ) +cmake_minimum_required( VERSION 2.8.6 ) +set( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules" ) + +find_package(Microhttpd REQUIRED) +find_package(Spotify REQUIRED) + +include(CheckCXXCompilerFlag) +check_cxx_compiler_flag( "-std=c++11" CXX11_FOUND ) +check_cxx_compiler_flag( "-std=c++0x" CXX0X_FOUND ) +check_cxx_compiler_flag( "-stdlib=libc++" LIBCPP_FOUND ) +if(CXX11_FOUND) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +elseif(CXX0X_FOUND) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") +else() + message(FATAL_ERROR "${CMAKE_CXX_COMPILER} does not support C++11, please + use a different compiler") +endif() +if(LIBCPP_FOUND AND APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") +endif() + +# We want as many as possible warnings +set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") + +include_directories( + ${MICROHTTPD_INCLUDE_DIR} +) + +# Define the "resolver" target +add_executable( spotify_native_bin + main.cpp +) +target_link_libraries( spotify_native_bin + ${MICROHTTPD_LIBRARY} +) +set_target_properties( spotify_native_bin + PROPERTIES + RUNTIME_OUTPUT_NAME spotify-native${SPOTIFY_NATIVE_SUFFIX} +) diff --git a/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindMicrohttpd.cmake b/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindMicrohttpd.cmake new file mode 100644 index 000000000..a7eb73da5 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindMicrohttpd.cmake @@ -0,0 +1,22 @@ +find_package(PkgConfig) +pkg_check_modules(PC_MICROHTTPD libmicrohttpd) + +find_path(MICROHTTPD_INCLUDE_DIR microhttpd.h + HINTS + ${PC_MICROHTTPD_INCLUDEDIR} + ${PC_MICROHTTPD_INCLUDE_DIRS} +) + +find_library(MICROHTTPD_LIBRARY NAMES microhttpd + HINTS + ${PC_MICROHTTPD_LIBDIR} + ${PC_MICROHTTPD_LIBRARY_DIRS} +) + +set(MICROHTTPD_VERSION ${PC_MICROHTTPD_VERSION}) + +find_package_handle_standard_args(Microhttpd + REQUIRED_VARS MICROHTTPD_INCLUDE_DIR MICROHTTPD_LIBRARY + VERSION_VAR MICROHTTPD_VERSION +) + diff --git a/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindSpotify.cmake b/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindSpotify.cmake new file mode 100644 index 000000000..8fda243e2 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/native/CMakeModules/FindSpotify.cmake @@ -0,0 +1,22 @@ +find_package(PkgConfig) +pkg_check_modules(PC_SPOTIFY libspotify) + +find_path(SPOTIFY_INCLUDE_DIR libspotify/api.h + HINTS + ${PC_SPOTIFY_INCLUDEDIR} + ${PC_SPOTIFY_INCLUDE_DIRS} +) + +find_library(SPOTIFY_LIBRARY NAMES spotify + HINTS + ${PC_SPOTIFY_LIBDIR} + ${PC_SPOTIFY_LIBRARY_DIRS} +) + +set(SPOTIFY_VERSION ${PC_SPOTIFY_VERSION}) + +find_package_handle_standard_args(Spotify + REQUIRED_VARS SPOTIFY_INCLUDE_DIR SPOTIFY_LIBRARY + VERSION_VAR SPOTIFY_VERSION +) + diff --git a/app/src/main/assets/js/resolvers/spotify/native/main.cpp b/app/src/main/assets/js/resolvers/spotify/native/main.cpp new file mode 100644 index 000000000..34b34bf05 --- /dev/null +++ b/app/src/main/assets/js/resolvers/spotify/native/main.cpp @@ -0,0 +1,116 @@ +/* + * Copyright 2014, Uwe L. Korn + * + * The MIT License (MIT) + * + * 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. + */ + +#include + +#include +#include +#include + +// Typedef the pointers for better readability. +typedef struct MHD_Connection* connection_ptr; +typedef struct MHD_Daemon* daemon_ptr; +typedef struct MHD_Response* response_ptr ; + +std::mutex exit_mutex; + +int handle_exit(const connection_ptr connection) +{ + // Shutdown requested, unlock the relevant mutex. + response_ptr response = MHD_create_response_from_data( strlen( "OK" ), (void*)"OK", MHD_NO, MHD_NO ); + int ret = MHD_queue_response( connection, MHD_HTTP_OK, response ); + MHD_destroy_response( response ); + // FIXME: Add a settle time so that we can actually send the response. + exit_mutex.unlock(); + return ret; +} + +static int ahc_echo( void* /*cls*/, connection_ptr connection, const char* url, + const char* method, const char* /*version*/, const char* /*upload_data*/, + size_t* upload_data_size, void** ptr) +{ + static int header_test; + + // We only expect GET requests. + if ( strcmp( method, "GET" ) ) + { + return MHD_NO; + } + + // On the first call only the headers are valid. Reply in the second round. + if ( &header_test != *ptr ) + { + *ptr = &header_test; + return MHD_YES; + } + + // There should be no data uploaded in the GET request. + if ( *upload_data_size ) + { + return MHD_NO; + } + + int ret; + if ( !strcmp( url, "/exit" ) ) + { + ret = handle_exit(connection); + } + else + { + response_ptr response = MHD_create_response_from_data( strlen(url), (void*)url, MHD_NO, MHD_NO ); + ret = MHD_queue_response( connection, MHD_HTTP_OK, response ); + MHD_destroy_response( response ); + } + + return ret; +} + +int main( int argc, char* argv[] ) +{ + // TODO: Kill all other instances on startup. + + if ( argc != 2 ) { + std::cout << "Usage:" << std::endl; + std::cout << "\t" << argv[0] << " " << std::endl; + return EXIT_FAILURE; + } + + daemon_ptr daemon = MHD_start_daemon( MHD_USE_THREAD_PER_CONNECTION, + atoi(argv[1]), nullptr, nullptr, + &ahc_echo, nullptr, MHD_OPTION_END); + if ( daemon == nullptr ) + { + return EXIT_FAILURE; + } + + // Lock mutex twice. The second call will hang until we receive an unlock + // command from a different. This will initiate the shutdown process. + exit_mutex.lock(); + exit_mutex.lock(); + + // (void) getc (); + + MHD_stop_daemon(daemon); + + return EXIT_SUCCESS; +} diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/config.ui new file mode 100644 index 000000000..25a81e61d --- /dev/null +++ b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/config.ui @@ -0,0 +1,104 @@ + + + Form + + + + 0 + 0 + 331 + 250 + + + + + 331 + 250 + + + + Form + + + + + + subsonic.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Requires at least Subsonic version 4.7 + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Server URL + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Username: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Password: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.js b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.js new file mode 100644 index 000000000..b9c8b0020 --- /dev/null +++ b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.js @@ -0,0 +1,340 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, mack-t + * Copyright 2012, Peter Loron + * Copyright 2013, Teo Mrnjavac + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var SubsonicResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Subsonic', + icon: 'subsonic-icon.png', + weight: 70, + timeout: 8 + }, + + _subsonicApiVersion: "1.8.0", + + getConfigUi: function () { + var uiData = Tomahawk.readBase64("config.ui"); + return { + "widget": uiData, + fields: [{ + name: "subsonic_url", + widget: "subsonic_url_edit", + property: "text" + }, { + name: "user", + widget: "user_edit", + property: "text" + }, { + name: "password", + widget: "password_edit", + property: "text" + }], + images: [{ + "subsonic.png": Tomahawk.readBase64("subsonic.png") + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + type: "textview", + text: "Requires at least Subsonic version 4.7" + }, + { + id: "subsonic_url", + type: "textfield", + label: "Server URL", + defaultValue: "http://localhost:4040/" + }, + { + id: "user", + type: "textfield", + label: "Username" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + } + ], + + newConfigSaved: function (newConfig) { + Tomahawk.log("newConfigSaved User: " + newConfig.user); + + if (this.user !== newConfig.user || + this.password !== newConfig.password || + this.subsonic_url !== newConfig.subsonic_url) { + Tomahawk.log("Invalidating cache"); + var that = this; + subsonicCollection.wipe({id: subsonicCollection.settings.id}).then(function () { + window.localStorage.removeItem("subsonic_last_cache_update"); + that.init(); + }); + } + }, + + _hexEncode: function (string) { + var hex_slice; + var hex_string = ""; + var padding = ["", "0", "00"]; + for (var pos = 0; pos < string.length; hex_string += hex_slice) { + hex_slice = string.charCodeAt(pos++).toString(16); + hex_slice = hex_slice.length < 2 ? (padding[2 - hex_slice.length] + hex_slice) + : hex_slice; + } + return "enc:" + hex_string; + }, + + init: function () { + var userConfig = this._sanitizeConfig(this.getUserConfig()); + if (!userConfig.user || !userConfig.password) { + Tomahawk.log("Subsonic Resolver not properly configured!"); + return; + } + + this.user = userConfig.user; + this.user = this.user.trim(); + this.password = this._hexEncode(userConfig.password); + this.subsonic_url = userConfig.subsonic_url || ""; + + Tomahawk.log("Subsonic resolver initalized, got credentials from config. user: " + + this.user + ", subsonic_url: " + this.subsonic_url); + + this._ensureCollection(); + }, + + _sanitizeConfig: function (config) { + if (!config.subsonic_url) { + config.subsonic_url = "http://localhost:4040/"; + } else { + if (config.subsonic_url.search("^.*:\/\/") < 0) { + // couldn't find a proper protocol, so we default to "http://" + config.subsonic_url = "http://" + config.subsonic_url; + } + var url = new URL(config.subsonic_url); + if (!url.port) { + url.port = 4040; + } + config.subsonic_url = url.toString(); + } + + return config; + }, + + _buildStreamUrl: function (id) { + return this.subsonic_url + "/rest/stream.view" + + "?u=" + this.user + + "&p=" + this.password + + "&v=" + this._subsonicApiVersion + + "&c=tomahawk" + + "&f=json" + + "&id=" + id; + }, + + _convertTracks: function (results) { + var tracks = []; + for (var i = 0; results && i < results.length; i++) { + var result = results[i]; + if (!result.isDir && !result.isVideo && result.type == "music") { + tracks.push({ + artist: result.artist, + album: result.album, + track: result.title, + albumpos: result.track, + source: this.settings.name, + size: result.size, + duration: result.duration, + bitrate: result.bitRate, + url: this._buildStreamUrl(result.id), + extension: result.suffix, + year: result.year + }); + } + } + return tracks; + }, + + _ensureCollection: function () { + var that = this; + + return subsonicCollection.revision({ + id: subsonicCollection.settings.id + }).then(function (result) { + var lastCollectionUpdate = window.localStorage["subsonic_last_collection_update"]; + if (lastCollectionUpdate && lastCollectionUpdate == result) { + Tomahawk.log("Collection database has not been changed since last time."); + var ifModifiedSince; + if (window.localStorage["subsonic_last_cache_update"]) { + ifModifiedSince = window.localStorage["subsonic_last_cache_update"]; + } + return that._fetchAndStoreCollection(ifModifiedSince); + } else { + Tomahawk.log("Collection database has been changed. Wiping and re-fetching..."); + return subsonicCollection.wipe({ + id: subsonicCollection.settings.id + }).then(function () { + return that._fetchAndStoreCollection(); + }); + } + }); + }, + + _fetchAndStoreCollection: function (ifModifiedSince) { + var that = this; + + if (!this._requestPromise) { + Tomahawk.log("Checking if collection needs to be updated"); + var time = Date.now(); + + var url = this.subsonic_url + "/rest/getIndexes.view"; + var settings = { + data: { + c: "tomahawk", + f: "json", + p: this.password, + u: this.user, + v: this._subsonicApiVersion + } + }; + if (ifModifiedSince) { + settings.data.ifModifiedSince = ifModifiedSince; + } + this._requestPromise = Tomahawk.get(url, settings).then(function (response) { + Tomahawk.PluginManager.registerPlugin("collection", subsonicCollection); + var artists = response["subsonic-response"].indexes; + if (artists) { + Tomahawk.log("Collection needs to be updated"); + + var promises = []; + for (var i = 0; i < artists.index.length; i++) { + var directoryId = artists.index[i].artist[0].id; + var url = that.subsonic_url + "/rest/getMusicDirectory.view"; + var settings = { + data: { + c: "tomahawk", + f: "json", + p: that.password, + u: that.user, + v: that._subsonicApiVersion, + id: directoryId + } + }; + var promise = Tomahawk.get(url, settings).then(function (response) { + return that._convertTracks( + response["subsonic-response"].directory.child); + }); + promises.push(promise); + } + + RSVP.all(promises).then(function (tracksList) { + var tracks = []; + for (var i = 0; i < tracksList.length; i++) { + tracks = tracks.concat(tracksList[i]); + } + subsonicCollection.addTracks({ + id: subsonicCollection.settings.id, + tracks: tracks + }).then(function (newRevision) { + Tomahawk.log("Updated cache in " + (Date.now() - time) + "ms"); + window.localStorage["subsonic_last_cache_update"] + = response["subsonic-response"].indexes.lastModified; + window.localStorage["subsonic_last_collection_update"] = newRevision; + }); + }); + } else { + Tomahawk.log("Collection doesn't need to be updated"); + subsonicCollection.addTracks({ + id: subsonicCollection.settings.id, + tracks: [] + }); + } + }, function (xhr) { + Tomahawk.log("Tomahawk.get failed: " + xhr.status + " - " + + xhr.statusText + " - " + xhr.responseText); + }).finally(function () { + that._requestPromise = undefined; + }); + } + return this._requestPromise; + }, + + testConfig: function (config) { + config = this._sanitizeConfig(config); + var url = config.subsonic_url + "/rest/ping.view"; + var settings = { + data: { + u: config.user, + p: config.password, + v: this._subsonicApiVersion, + c: "tomahawk", + f: "json" + } + }; + return Tomahawk.get(url, settings).then(function (response) { + if (response && response["subsonic-response"] + && response["subsonic-response"].status) { + if (response["subsonic-response"].status === "ok") { + return Tomahawk.ConfigTestResultType.Success; + } else { + if (response["subsonic-response"].error) { + if (response["subsonic-response"].error.code === 40) { + return Tomahawk.ConfigTestResultType.InvalidCredentials; + } else if (response["subsonic-response"].error.code === 50) { + return Tomahawk.ConfigTestResultType.InvalidAccount; + } else if (response["subsonic-response"].error.message) { + return response["subsonic-response"].error.message; + } + } else { + return Tomahawk.ConfigTestResultType.CommunicationError; + } + } + } else { + return Tomahawk.ConfigTestResultType.CommunicationError; + } + }, function (xhr) { + if (xhr.status == 404 || xhr.status == 0) { + return Tomahawk.ConfigTestResultType.CommunicationError; + } else { + return xhr.responseText.trim(); + } + } + ); + } + +}); + +Tomahawk.resolver.instance = SubsonicResolver; + +var subsonicCollection = Tomahawk.extend(Tomahawk.Collection, { + settings: { + id: "subsonic", + prettyname: "Subsonic", + description: SubsonicResolver.subsonic_url, + iconfile: "contents/images/icon.png", + trackcount: 0 + } +}); \ No newline at end of file diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.png b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.png new file mode 100644 index 000000000..816e94fb3 Binary files /dev/null and b/app/src/main/assets/js/resolvers/subsonic/content/contents/code/subsonic.png differ diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/icon.png new file mode 100644 index 000000000..a95a8d5c7 Binary files /dev/null and b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconBackground.png new file mode 100644 index 000000000..18d455d63 Binary files /dev/null and b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconWhite.png new file mode 100644 index 000000000..2e56086db Binary files /dev/null and b/app/src/main/assets/js/resolvers/subsonic/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/subsonic/content/metadata.json b/app/src/main/assets/js/resolvers/subsonic/content/metadata.json new file mode 100644 index 000000000..84cdbfb82 --- /dev/null +++ b/app/src/main/assets/js/resolvers/subsonic/content/metadata.json @@ -0,0 +1,26 @@ +{ + "name": "Subsonic", + "pluginName": "subsonic", + "author": "mack_t, Teo and Enno", + "email": "teo@kde.org", + "version": "0.9.1", + "website": "http://gettomahawk.com", + "description": "Searches your Subsonic server for music to play.", + "platform": "any", + "tomahawkVersion": "0.6.99", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/subsonic.js", + "scripts": [], + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "resources": [ + "contents/code/config.ui", + "contents/code/subsonic.png" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/tidal/content/contents/code/config.ui b/app/src/main/assets/js/resolvers/tidal/content/contents/code/config.ui new file mode 100644 index 000000000..87fcd8104 --- /dev/null +++ b/app/src/main/assets/js/resolvers/tidal/content/contents/code/config.ui @@ -0,0 +1,90 @@ + + + Form + + + + 0 + 0 + 390 + 120 + + + + + 250 + 120 + + + + Form + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Email + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Password + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + Stream Quality + + + + + + + + LOW + + + + + HIGH + + + + + LOSSLESS + + + + + + + + + + + diff --git a/app/src/main/assets/js/resolvers/tidal/content/contents/code/tidal.js b/app/src/main/assets/js/resolvers/tidal/content/contents/code/tidal.js new file mode 100755 index 000000000..16f65c7f7 --- /dev/null +++ b/app/src/main/assets/js/resolvers/tidal/content/contents/code/tidal.js @@ -0,0 +1,446 @@ +/* Tidal resolver for Tomahawk. + * + * Written in 2015 by Anton Romanov, and Will Stott + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication + * along with this software. If not, see: + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var TidalResolver = Tomahawk.extend(Tomahawk.Resolver, { + apiVersion: 0.9, + + /* This can also be used with WiMP service if you change next 2 lines */ + api_location: 'https://listen.tidal.com/v1/', + api_token: 'P5Xbeo5LFvESeDy6', + + logged_in: null, // null, = not yet tried, 0 = pending, 1 = success, 2 = failed + + settings: { + cacheTime: 300, + name: 'TIDAL', + icon: '../images/icon.png', + weight: 91, + timeout: 8 + }, + + strQuality: ['LOW', 'HIGH', 'LOSSLESS'], + numQuality: [64, 320, 1411], + + getConfigUi: function () { + return { + "widget": Tomahawk.readBase64("config.ui"), + fields: [{ + name: "email", + widget: "email_edit", + property: "text" + }, { + name: "password", + widget: "password_edit", + property: "text" + }, { + name: "quality", + widget: "quality", + property: "currentIndex" + }] + }; + }, + + /** + * Defines this Resolver's config dialog UI. + */ + configUi: [ + { + id: "email", + type: "textfield", + label: "E-Mail" + }, + { + id: "password", + type: "textfield", + label: "Password", + isPassword: true + }, + { + id: "quality", + type: "dropdown", + label: "Audio quality", + items: ["Low", "High", "Lossless"], + defaultValue: 2 + } + ], + + newConfigSaved: function (newConfig) { + var changed = + this._email !== newConfig.email || + this._password !== newConfig.password || + this._quality != newConfig.quality; + + if (changed) { + this.init(); + } + }, + + testConfig: function (config) { + return this._getLoginPromise(config).then(function () { + return Tomahawk.ConfigTestResultType.Success; + }, function (xhr) { + if (xhr.status == 401) { + return Tomahawk.ConfigTestResultType.InvalidCredentials; + } else { + return Tomahawk.ConfigTestResultType.CommunicationError; + } + }); + }, + + init: function () { + var config = this.getUserConfig(); + + this._email = config.email; + this._password = config.password; + this._quality = config.quality; + + if (!this._email || !this._password) { + Tomahawk.PluginManager.unregisterPlugin("linkParser", this); + //This is being called even for disabled ones + //throw new Error( "Invalid configuration." ); + Tomahawk.log("Invalid Configuration"); + return; + } + + Tomahawk.PluginManager.registerPlugin("linkParser", this); + + this._login(config); + }, + + _convertTracks: function (entries) { + return entries.filter(function (entry) { + return entry.allowStreaming; + }).map(this._convertTrack, this); + }, + + _convertTrack: function (entry) { + return { + type: Tomahawk.UrlType.Track, + artist: entry.artist.name, + album: entry.album.title, + track: entry.title, + year: entry.year, + + albumpos: entry.trackNumber, + discnumber: entry.volumeNumber, + + duration: entry.duration, + + url: 'tidal://track/' + entry.id, + hint: 'tidal://track/' + entry.id, + checked: true, + bitrate: this.numQuality[this._quality] + }; + }, + + _convertAlbum: function (entry) { + return { + type: Tomahawk.UrlType.Album, + artist: entry.artist.name, + album: entry.title, + url: entry.url + }; + }, + + _convertArtist: function (entry) { + return { + type: Tomahawk.UrlType.Artist, + artist: entry.name + }; + }, + + _convertPlaylist: function (entry) { + return { + type: Tomahawk.UrlType.Playlist, + title: entry.title, + guid: "tidal-playlist-" + entry.uuid, + info: entry.description + " (from TidalHiFi)", + creator: "tidal-user-" + entry.creator.id, + // TODO: Perhaps use tidal://playlist/uuid + url: entry.url + }; + }, + + search: function (params) { + var query = params.query; + var limit = params.limit; + + if (!this.logged_in) { + return this._defer(this.search, [query], this); + } else if (this.logged_in === 2) { + throw new Error('Failed login, cannot search.'); + } + + var that = this; + var settings = { + data: { + limit: limit || 9999, + query: query.replace(/[ \-]+/g, ' ').toLowerCase(), + + sessionId: this._sessionId, + countryCode: this._countryCode + } + }; + return Tomahawk.get(this.api_location + "search/tracks", settings) + .then(function (response) { + return that._convertTracks(response.items); + }); + }, + + resolve: function (params) { + var artist = params.artist; + var album = params.album; + var track = params.track; + + var query = [artist, track].join(' '); + + return this.search({ + query: query, + limit: 5 + }); + }, + + /** + * Splits the given url into 3 parts. see http://www.regexr.com/3ahue + * Returns array containing: + * [1]: 'tidal' or 'wimpmusic' + * [2]: 'artist' or 'album' or 'track' or 'playlist' (removes the s) + * [3]: ID of resource (seems to be the same for both services!) + */ + _parseUrlPrefix: function (url) { + return url.match(/(?:https?:\/\/)?(?:listen|play|www)\.(tidal|wimpmusic)\.com\/(?:v1\/)?([a-z]{3,}?)s?\/([\w\-]+)[\/?]?/); + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + url = this._parseUrlPrefix(url); + if (!url) { + throw new Error("Couldn't parse URL. Invalid format?"); + } + switch (type) { + case Tomahawk.UrlType.Album: + return url[2] == 'album'; + case Tomahawk.UrlType.Artist: + return url[2] == 'artist'; + case Tomahawk.UrlType.Track: + return url[2] == 'track'; + case Tomahawk.UrlType.Playlist: + return url[2] == 'playlist'; + } + }, + + _debugPrint: function (obj, spaces) { + spaces = spaces || ''; + + var str = ''; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + var b = ["{", "}"]; + if (obj[key].constructor == Array) { + b = ["[", "]"]; + } + str += spaces + key + ": " + b[0] + "\n" + this._debugPrint(obj[key], + spaces + ' ') + "\n" + spaces + b[1] + '\n'; + } else { + str += spaces + key + ": " + obj[key] + "\n"; + } + } + if (spaces != '') { + return str; + } else { + str.split('\n').map(Tomahawk.log, Tomahawk); + } + }, + + lookupUrl: function (params) { + var url = params.url; + + return this._getLookupUrlPromise(url); + }, + + _getLookupUrlPromise: function (url) { + if (!this.logged_in) { + return this._defer(this.lookupUrl, [url], this); + } else if (this.logged_in === 2) { + throw new Error('Failed login, cannot lookupUrl'); + } + + var match = this._parseUrlPrefix(url); + + Tomahawk.log(url + " -> " + match[1] + " " + match[2] + " " + match[3]); + + if (!match[1]) { + throw new Error("Couldn't parse given URL: " + url); + } + + var that = this; + + var params = { + countryCode: this._countryCode, + sessionId: this._sessionId, + limit: 9999 + }; + + if (match[2] == 'album') { + var rqUrl = this.api_location + 'albums/' + match[3]; + + var getInfo = Tomahawk.get(rqUrl, {data: params}); + var getTracks = Tomahawk.get(rqUrl + "/tracks", {data: params}); + + Tomahawk.log(rqUrl); + + return RSVP.Promise.all([getInfo, getTracks]).then(function (response) { + var result = that._convertAlbum(response[0]); + result.tracks = that._convertTracks(response[1].items); + return result; + }); + + } else if (match[2] == 'artist') { + var rqUrl = this.api_location + 'artists/' + match[3]; + + return Tomahawk.get(rqUrl, { + data: params + }).then(function (response) { + return that._convertArtist(response); + }); + + } else if (match[2] == 'track') { + var rqUrl = this.api_location + 'tracks/' + match[3]; + // I can't find any link on the site for tracks. + return Tomahawk.get(rqUrl, { + data: params + }).then(function (response) { + return that._convertTrack(response); + }); + + } else if (match[2] == 'playlist') { + var rqUrl = this.api_location + 'playlists/' + match[3]; + + var getInfo = Tomahawk.get(rqUrl, {data: params}); + var getTracks = Tomahawk.get(rqUrl + "/tracks", {data: params}); + + return RSVP.Promise.all([getInfo, getTracks]).then(function (response) { + var result = that._convertPlaylist(response[0]); + result.tracks = that._convertTracks(response[1].items); + return result; + }); + } + }, + + _parseUrn: function (urn) { + // "tidal://track/18692667" + var match = urn.match(/^tidal:\/\/([a-z]+)\/(.+)$/); + if (!match) { + return null; + } + + return { + type: match[1], + id: match[2] + }; + }, + + getStreamUrl: function (params) { + var url = params.url; + + if (!this.logged_in) { + return this._defer(this.getStreamUrl, [url], this); + } else if (this.logged_in === 2) { + throw new Error('Failed login, cannot getStreamUrl.'); + } + + var parsedUrn = this._parseUrn(url); + + if (!parsedUrn || parsedUrn.type != 'track') { + Tomahawk.log("Failed to get stream. Couldn't parse '" + url + "'"); + return; + } + + var settings = { + data: { + token: this.api_token, + countryCode: this._countryCode, + soundQuality: this.strQuality[this._quality], + sessionId: this._sessionId + } + }; + + return Tomahawk.get(this.api_location + "tracks/" + parsedUrn.id + "/streamUrl", settings) + .then(function (response) { + return { + url: response.url + }; + }); + }, + + _defer: function (callback, args, scope) { + if (typeof this._loginPromise !== 'undefined' && 'then' in this._loginPromise) { + args = args || []; + scope = scope || this; + Tomahawk.log('Deferring action with ' + args.length + ' arguments.'); + return this._loginPromise.then(function () { + Tomahawk.log('Performing deferred action with ' + args.length + ' arguments.'); + callback.call(scope, args); + }); + } + }, + + _getLoginPromise: function (config) { + var settings = { + type: 'POST', // backwards compatibility for old versions of tomahawk.js + data: { + "username": config.email.trim(), + "password": config.password.trim() + }, + headers: {'Origin': 'http://listen.tidal.com'} + }; + return Tomahawk.post(this.api_location + "login/username?token=" + this.api_token, + settings); + }, + + _login: function (config) { + // If a login is already in progress don't start another! + if (this.logged_in === 0) { + return; + } + this.logged_in = 0; + + var that = this; + + this._loginPromise = this._getLoginPromise(config) + .then(function (resp) { + Tomahawk.log(that.settings.name + " successfully logged in."); + + that._countryCode = resp.countryCode; + that._sessionId = resp.sessionId; + that._userId = resp.userId; + + that.logged_in = 1; + }, function (error) { + Tomahawk.log(that.settings.name + " failed login."); + + delete that._countryCode; + delete that._sessionId; + delete that._userId; + + that.logged_in = 2; + } + ); + return this._loginPromise; + } +}); + +Tomahawk.resolver.instance = TidalResolver; diff --git a/app/src/main/assets/js/resolvers/tidal/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/tidal/content/contents/images/icon.png new file mode 100644 index 000000000..53f777ec7 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tidal/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconBackground.png b/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconBackground.png new file mode 100644 index 000000000..7ddc2a741 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconBackground.png differ diff --git a/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconWhite.png b/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconWhite.png new file mode 100644 index 000000000..51a1991e9 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tidal/content/contents/images/iconWhite.png differ diff --git a/app/src/main/assets/js/resolvers/tidal/content/metadata.json b/app/src/main/assets/js/resolvers/tidal/content/metadata.json new file mode 100644 index 000000000..b9046eea3 --- /dev/null +++ b/app/src/main/assets/js/resolvers/tidal/content/metadata.json @@ -0,0 +1,23 @@ +{ + "name": "TIDAL", + "pluginName": "tidal", + "author": "Anton Romanov and Will Stott", + "email": "", + "version": "0.0.7", + "website": "http://gettomahawk.com", + "description": "Streams music from tidal.com (requires subscription)", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/tidal.js", + "icon": "contents/images/icon.png", + "iconWhite": "contents/images/iconWhite.png", + "iconBackground": "contents/images/iconBackground.png", + "scripts": [], + "resources": [ + "contents/code/config.ui" + ] + }, + "staticCapabilities": [ + "configTestable" + ] +} diff --git a/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.js b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.js new file mode 100644 index 000000000..9c45cc84c --- /dev/null +++ b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.js @@ -0,0 +1,89 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ + +var TomahawkMetadataResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'Tomahawk Metadata', + icon: 'tomahawk-metadata.png', + weight: 0, // We cannot resolve, so use minimum weight + timeout: 15 + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + switch (type) { + case Tomahawk.UrlType.Album: + return /^tomahawk:\/\/view\/album\/?\?$/.test(url); + case Tomahawk.UrlType.Artist: + return /^tomahawk:\/\/view\/artist\/?\?$/.test(url); + case Tomahawk.UrlType.Track: + return /^tomahawk:\/\/(queue\/add|play)\/track\/?\?$/.test(url); + default: + return false; + } + }, + + lookupUrl: function (params) { + var url = params.url; + + Tomahawk.log("lookupUrl: " + url); + if (/^tomahawk:\/\/view\/album\/?\?$/.test(url)) { + Tomahawk.log("Found an album"); + // We have to deal with an Album + return { + type: Tomahawk.UrlType.Album, + artist: this._getQueryVariable(url, 'artist'), + album: this._getQueryVariable(url, 'name') + }; + } else if (/^tomahawk:\/\/view\/artist\/?\?$/.test(url)) { + Tomahawk.log("Found an artist"); + // We have to deal with an Artist + return { + type: Tomahawk.UrlType.Artist, + artist: this._getQueryVariable(url, 'name') + }; + } else if (/^tomahawk:\/\/(queue\/add|play)\/track\/?\?$/.test(url)) { + Tomahawk.log("Found a track"); + // We have to deal with a Track + return { + type: Tomahawk.UrlType.Track, + artist: this._getQueryVariable(url, 'artist'), + track: this._getQueryVariable(url, 'title') + }; + } + }, + + _getQueryVariable: function (url, variable) { + var parts = url.split('?'); + var vars = parts[parts.length - 1].split('&'); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } + } +}); + +Tomahawk.resolver.instance = TomahawkMetadataResolver; + diff --git a/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.png b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.png new file mode 100644 index 000000000..8461e9952 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/code/tomahawk-metadata.png differ diff --git a/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/images/icon.png new file mode 100644 index 000000000..c4ccf7b30 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/tomahawk-metadata/content/metadata.json b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/metadata.json new file mode 100644 index 000000000..cc2f5936d --- /dev/null +++ b/app/src/main/assets/js/resolvers/tomahawk-metadata/content/metadata.json @@ -0,0 +1,18 @@ +{ + "name": "tomahawk Metadata", + "pluginName": "tomahawk-metadata", + "author": "Enno Gottschalk", + "email": "mrmaffen@googlemail.com", + "version": "0.2.0", + "website": "http://gettomahawk.com", + "description": "Supports loading/drag'n'drop of tomahawk:// URLs.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/tomahawk-metadata.js", + "scripts": [], + "icon": "contents/images/icon.png", + "resources": [ + "contents/code/tomahawk-metadata.png" + ] + } +} diff --git a/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.js b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.js new file mode 100644 index 000000000..9df6288fe --- /dev/null +++ b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.js @@ -0,0 +1,98 @@ +/* + * Copyright 2013, Uwe L. Korn + * Copyright 2014, Enno Gottschalk + * + * 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. + */ + +var TomaHKMetadataResolver = Tomahawk.extend(Tomahawk.Resolver, { + + apiVersion: 0.9, + + settings: { + name: 'toma.hk Metadata', + icon: 'tomahk-metadata.png', + weight: 0, // We cannot resolve, so use minimum weight + timeout: 15 + }, + + canParseUrl: function (params) { + var url = params.url; + var type = params.type; + + switch (type) { + case Tomahawk.UrlType.Album: + return /https?:\/\/(www\.)?toma.hk\/album\//.test(url); + case Tomahawk.UrlType.Artist: + return /https?:\/\/(www\.)?toma.hk\/artist\//.test(url); + case Tomahawk.UrlType.Playlist: + return /https?:\/\/(www\.)?toma.hk\/p\//.test(url); + default: + return /https?:\/\/(www\.)?toma.hk\//.test(url); + } + }, + + lookupUrl: function (params) { + var url = params.url; + + var urlParts = + url.split('/').filter(function (item) { + return item.length != 0; + }).map(function (s) { + return decodeURIComponent(s.replace(/\+/g, '%20')); + }); + if (/https?:\/\/(www\.)?toma.hk\/album\//.test(url)) { + // We have to deal with an Album + return { + type: Tomahawk.UrlType.Album, + artist: urlParts[urlParts.length - 2], + album: urlParts[urlParts.length - 1] + }; + } else if (/https?:\/\/(www\.)?toma.hk\/artist\//.test(url)) { + // We have to deal with an Artist + return { + type: Tomahawk.UrlType.Artist, + artist: urlParts[urlParts.length - 1] + }; + } else if (/https?:\/\/(www\.)?toma.hk\/p\//.test(url)) { + // We have a xspf playlist + return { + type: Tomahawk.UrlType.XspfPlaylist, + url: url.replace('toma.hk/p/', 'toma.hk/xspf/') + }; + } else if (/https?:\/\/(www\.)?toma\.hk.*(\?title=)[^&]*(&artist=)/.test(url) + || /https?:\/\/(www\.)?toma\.hk.*(\?artist=)[^&]*(&title=)/.test(url)) { + // We search for a track + var artist = url.match(/(?:\?|&)artist=([^&]*)/)[1]; + var title = url.match(/(?:\?|&)title=([^&]*)/)[1]; + return { + type: Tomahawk.UrlType.Track, + artist: decodeURIComponent(artist.replace(/\+/g, '%20')), + track: decodeURIComponent(title.replace(/\+/g, '%20')) + }; + } else { + // We most likely have a track + var query = url.replace("http://toma.hk/", "http://toma.hk/api.php?id="); + return Tomahawk.get(query).then(function (res) { + if (res.artist.length > 0 && res.title.length > 0) { + return { + type: Tomahawk.UrlType.Track, + artist: res.artist, + track: res.title + }; + } + }); + } + } +}); + +Tomahawk.resolver.instance = TomaHKMetadataResolver; + diff --git a/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.png b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.png new file mode 100644 index 000000000..6b8155b43 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/code/tomahk-metadata.png differ diff --git a/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/images/icon.png b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/images/icon.png new file mode 100644 index 000000000..6b8155b43 Binary files /dev/null and b/app/src/main/assets/js/resolvers/tomahk-metadata/content/contents/images/icon.png differ diff --git a/app/src/main/assets/js/resolvers/tomahk-metadata/content/metadata.json b/app/src/main/assets/js/resolvers/tomahk-metadata/content/metadata.json new file mode 100644 index 000000000..4332a94e8 --- /dev/null +++ b/app/src/main/assets/js/resolvers/tomahk-metadata/content/metadata.json @@ -0,0 +1,18 @@ +{ + "name": "toma.hk Metadata", + "pluginName": "tomahk-metadata", + "author": "Uwe L. Korn and Enno", + "email": "uwelk@xhochy.com", + "version": "0.2.2", + "website": "http://gettomahawk.com", + "description": "Supports loading and drag and drop of toma.hk URLs.", + "type": "resolver/javascript", + "manifest": { + "main": "contents/code/tomahk-metadata.js", + "scripts": [], + "icon": "contents/images/icon.png", + "resources": [ + "contents/code/tomahk-metadata.png" + ] + } +} diff --git a/app/src/main/assets/js/rsvp-latest.min.js b/app/src/main/assets/js/rsvp-latest.min.js new file mode 100644 index 000000000..43655318c --- /dev/null +++ b/app/src/main/assets/js/rsvp-latest.min.js @@ -0,0 +1,9 @@ +/*! + * @overview RSVP - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/tildeio/rsvp.js/master/LICENSE + * @version 3.0.17 + */ + +(function(){"use strict";function lib$rsvp$utils$$objectOrFunction(x){return typeof x==="function"||typeof x==="object"&&x!==null}function lib$rsvp$utils$$isFunction(x){return typeof x==="function"}function lib$rsvp$utils$$isMaybeThenable(x){return typeof x==="object"&&x!==null}var lib$rsvp$utils$$_isArray;if(!Array.isArray){lib$rsvp$utils$$_isArray=function(x){return Object.prototype.toString.call(x)==="[object Array]"}}else{lib$rsvp$utils$$_isArray=Array.isArray}var lib$rsvp$utils$$isArray=lib$rsvp$utils$$_isArray;var lib$rsvp$utils$$now=Date.now||function(){return(new Date).getTime()};function lib$rsvp$utils$$F(){}var lib$rsvp$utils$$o_create=Object.create||function(o){if(arguments.length>1){throw new Error("Second argument not supported")}if(typeof o!=="object"){throw new TypeError("Argument must be an object")}lib$rsvp$utils$$F.prototype=o;return new lib$rsvp$utils$$F};function lib$rsvp$events$$indexOf(callbacks,callback){for(var i=0,l=callbacks.length;i === + * + * Copyright 2011, Dominik Schmidt + * Copyright 2011-2012, Leo Franchi + * Copyright 2011, Thierry Goeckel + * Copyright 2013, Teo Mrnjavac + * Copyright 2013-2014, Uwe L. Korn + * Copyright 2014, Enno Gottschalk + * + * 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. + */ + +// if run in phantomjs add fake Tomahawk environment +if ((typeof Tomahawk === "undefined") || (Tomahawk === null)) { + var Tomahawk = { + fakeEnv: function () { + return true; + }, + resolverData: function () { + return { + scriptPath: function () { + return "/home/tomahawk/resolver.js"; + } + }; + }, + log: function () { + console.log.apply(arguments); + } + }; +} + +Tomahawk.apiVersion = "0.2.2"; + +//Statuses considered a success for HTTP request +var httpSuccessStatuses = [200, 201]; + +Tomahawk.error = console.error; + +// install RSVP error handler for uncaught(!) errors +RSVP.on('error', function (reason) { + var resolverName = ""; + if (Tomahawk.resolver.instance) { + resolverName = Tomahawk.resolver.instance.settings.name + " - "; + } + if (reason) { + Tomahawk.error(resolverName + 'Uncaught error:', reason); + } else { + Tomahawk.error(resolverName + 'Uncaught error: error thrown from RSVP but it was empty'); + } +}); + +/** + * Compares versions strings + * (version1 < version2) == -1 + * (version1 = version2) == 0 + * (version1 > version2) == 1 + */ +Tomahawk.versionCompare = function (version1, version2) { + var v1 = version1.split('.').map(function (item) { + return parseInt(item); + }); + var v2 = version2.split('.').map(function (item) { + return parseInt(item); + }); + var length = Math.max(v1.length, v2.length); + var i = 0; + + for (; i < length; i++) { + if (typeof v1[i] == "undefined" || v1[i] === null) { + if (typeof v2[i] == "undefined" || v2[i] === null) { + // v1 == v2 + return 0; + } else if (v2[i] === 0) { + continue; + } else { + // v1 < v2 + return -1; + } + } else if (typeof v2[i] == "undefined" || v2[i] === null) { + if (v1[i] === 0) { + continue; + } else { + // v1 > v2 + return 1; + } + } else if (v2[i] > v1[i]) { + // v1 < v2 + return -1; + } else if (v2[i] < v1[i]) { + // v1 > v2 + return 1; + } + } + // v1 == v2 + return 0; +}; + +/** + * Check if this is at least specified tomahawk-api-version. + */ +Tomahawk.atLeastVersion = function (version) { + return (Tomahawk.versionCompare(Tomahawk.apiVersion, version) >= 0); +}; + +Tomahawk.resolver = { + scriptPath: Tomahawk.resolverData().scriptPath +}; + +Tomahawk.timestamp = function () { + return Math.round(new Date() / 1000); +}; + +Tomahawk.htmlDecode = (function () { + // this prevents any overhead from creating the object each time + var element = document.createElement('textarea'); + + function decodeHTMLEntities(str) { + if (str && typeof str === 'string') { + str = str.replace(//g, ">"); + element.innerHTML = str; + str = element.textContent; + element.textContent = ''; + } + + return str; + } + + return decodeHTMLEntities; +})(); + +Tomahawk.dumpResult = function (result) { + var results = result.results; + Tomahawk.log("Dumping " + results.length + " results for query " + result.qid + "..."); + for (var i = 0; i < results.length; i++) { + Tomahawk.log(results[i].artist + " - " + results[i].track + " | " + results[i].url); + } + + Tomahawk.log("Done."); +}; + +// javascript part of Tomahawk-Object API +Tomahawk.extend = function (object, members) { + var F = function () {}; + F.prototype = object; + var newObject = new F(); + + for (var key in members) { + newObject[key] = members[key]; + } + + return newObject; +}; + +//Deprecated for 0.9 resolvers. Reporting resolver capabilities is no longer necessary. +var TomahawkResolverCapability = { + NullCapability: 0, + Browsable: 1, + PlaylistSync: 2, + AccountFactory: 4, + UrlLookup: 8 +}; + +//Deprecated for 0.9 resolvers. Use Tomahawk.UrlType instead. +var TomahawkUrlType = { + Any: 0, + Playlist: 1, + Track: 2, + Album: 4, + Artist: 8, + Xspf: 16 +}; + +//Deprecated for 0.9 resolvers. Use Tomahawk.ConfigTestResultType instead. +var TomahawkConfigTestResultType = { + Other: 0, + Success: 1, + Logout: 2, + CommunicationError: 3, + InvalidCredentials: 4, + InvalidAccount: 5, + PlayingElsewhere: 6, + AccountExpired: 7 +}; + +/** + * Resolver BaseObject, inherit it to implement your own resolver. + */ +var TomahawkResolver = { + init: function () { + }, + scriptPath: function () { + return Tomahawk.resolverData().scriptPath; + }, + getConfigUi: function () { + return {}; + }, + getUserConfig: function () { + return JSON.parse(window.localStorage[this.scriptPath()] || "{}"); + }, + saveUserConfig: function () { + var configJson = JSON.stringify(Tomahawk.resolverData().config); + window.localStorage[this.scriptPath()] = configJson; + this.newConfigSaved(); + }, + newConfigSaved: function () { + }, + resolve: function (qid, artist, album, title) { + return { + qid: qid + }; + }, + search: function (qid, searchString) { + return this.resolve(qid, "", "", searchString); + }, + artists: function (qid) { + return { + qid: qid + }; + }, + albums: function (qid, artist) { + return { + qid: qid + }; + }, + tracks: function (qid, artist, album) { + return { + qid: qid + }; + }, + collection: function () { + return {}; + }, + _adapter_testConfig: function (config) { + return RSVP.Promise.resolve(this.testConfig(config)).then(function () { + return {result: Tomahawk.ConfigTestResultType.Success}; + }); + }, + testConfig: function () { + this.configTest(); + }, + getStreamUrl: function (qid, url) { + Tomahawk.reportStreamUrl(qid, url); + } +}; + +Tomahawk.Resolver = { + init: function () { + }, + scriptPath: function () { + return Tomahawk.resolverData().scriptPath; + }, + getConfigUi: function () { + return {}; + }, + getUserConfig: function () { + return JSON.parse(window.localStorage[this.scriptPath()] || "{}"); + }, + saveUserConfig: function () { + window.localStorage[this.scriptPath()] = JSON.stringify(Tomahawk.resolverData().config); + this.newConfigSaved(Tomahawk.resolverData().config); + }, + newConfigSaved: function () { + }, + testConfig: function () { + }, + getStreamUrl: function (params) { + return params; + }, + resolve: function() { + }, + _adapter_resolve: function (params) { + return RSVP.Promise.resolve(this.resolve(params)).then(function (results) { + if(Array.isArray(results)) { + return { + 'tracks': results + }; + } + + return results; + }); + }, + + _adapter_search: function (params) { + return RSVP.Promise.resolve(this.search(params)).then(function (results) { + if(Array.isArray(results)) { + return { + 'tracks': results + }; + } + + return results; + }); + }, + + _adapter_testConfig: function (config) { + return RSVP.Promise.resolve(this.testConfig(config)).then(function (results) { + results = results || Tomahawk.ConfigTestResultType.Success; + return results; + }, function (error) { + return error; + }); + } +}; + +// help functions + +Tomahawk.valueForSubNode = function (node, tag) { + if (node === undefined) { + throw new Error("Tomahawk.valueForSubnode: node is undefined!"); + } + + var element = node.getElementsByTagName(tag)[0]; + if (element === undefined) { + return undefined; + } + + return element.textContent; +}; + +/** + * Internal counter used to identify retrievedMetadata call back from native + * code. + */ +Tomahawk.retrieveMetadataIdCounter = 0; +/** + * Internal map used to map metadataIds to the respective JavaScript callbacks. + */ +Tomahawk.retrieveMetadataCallbacks = {}; + +/** + * Retrieve metadata for a media stream. + * + * @param url String The URL which should be scanned for metadata. + * @param mimetype String The mimetype of the stream, e.g. application/ogg + * @param sizehint Size in bytes if not supplied possibly the whole file needs + * to be downloaded + * @param options Object Map to specify various parameters related to the media + * URL. This includes: + * * headers: Object of HTTP(S) headers that should be set on doing the + * request. + * * method: String HTTP verb to be used (default: GET) + * * username: Username when using authentication + * * password: Password when using authentication + * @param callback Function(Object,String) This function is called on completeion. + * If an error occured, error is set to the corresponding message else + * null. + */ +Tomahawk.retrieveMetadata = function (url, mimetype, sizehint, options, callback) { + var metadataId = Tomahawk.retrieveMetadataIdCounter; + Tomahawk.retrieveMetadataIdCounter++; + Tomahawk.retrieveMetadataCallbacks[metadataId] = callback; + Tomahawk.nativeRetrieveMetadata(metadataId, url, mimetype, sizehint, options); +}; + +/** + * Pass the natively retrieved metadata back to the JavaScript callback. + * + * Internal use only! + */ +Tomahawk.retrievedMetadata = function (metadataId, metadata, error) { + // Check that we have a matching callback stored. + if (!Tomahawk.retrieveMetadataCallbacks.hasOwnProperty(metadataId)) { + return; + } + + // Call the real callback + if (Tomahawk.retrieveMetadataCallbacks.hasOwnProperty(metadataId)) { + Tomahawk.retrieveMetadataCallbacks[metadataId](metadata, error); + } + + // Callback are only used once. + delete Tomahawk.retrieveMetadataCallbacks[metadataId]; +}; + +/** + * This method is externalized from Tomahawk.asyncRequest, so that other clients + * (like tomahawk-android) can inject their own logic that determines whether or not to do a request + * natively. + * + * @returns boolean indicating whether or not to do a request with the given parameters natively + */ +var shouldDoNativeRequest = function (options) { + var extraHeaders = options.headers; + return (extraHeaders && (extraHeaders.hasOwnProperty("Referer") + || extraHeaders.hasOwnProperty("referer") + || extraHeaders.hasOwnProperty("User-Agent"))); +}; + +/** + * Possible options: + * - url: The URL to call + * - method: The HTTP request method (default: GET) + * - username: The username for HTTP Basic Auth + * - password: The password for HTTP Basic Auth + * - errorHandler: callback called if the request was not completed + * - data: body data included in POST requests + * - needCookieHeader: boolean indicating whether or not the request needs to be able to get the + * "Set-Cookie" response header + * - headers: headers set on the request + */ +var doRequest = function(options) { + if (shouldDoNativeRequest(options)) { + return Tomahawk.NativeScriptJobManager.invoke('httpRequest', options).then(function(xhr) { + xhr.responseHeaders = xhr.responseHeaders || {}; + xhr.getAllResponseHeaders = function() { + return this.responseHeaders; + }; + xhr.getResponseHeader = function (header) { + for(key in xhr.responseHeaders) { + if(key.toLowerCase() === header.toLowerCase()) { + return xhr.responseHeaders[key]; + } + } + return null; + }; + + return xhr; + }); + } else { + return new RSVP.Promise(function(resolve, reject) { + var xmlHttpRequest = new XMLHttpRequest(); + xmlHttpRequest.open(options.method, options.url, true, options.username, options.password); + if (options.headers) { + for (var headerName in options.headers) { + xmlHttpRequest.setRequestHeader(headerName, options.headers[headerName]); + } + } + xmlHttpRequest.onreadystatechange = function () { + if (xmlHttpRequest.readyState == 4 + && httpSuccessStatuses.indexOf(xmlHttpRequest.status) != -1) { + resolve(xmlHttpRequest); + } else if (xmlHttpRequest.readyState === 4) { + Tomahawk.log("Failed to do " + options.method + " request: to: " + options.url); + Tomahawk.log("Status Code was: " + xmlHttpRequest.status); + reject(xmlHttpRequest); + } + }; + xmlHttpRequest.send(options.data || null); + }); + } +}; + +Tomahawk.ajax = function (url, settings) { + if (typeof url === "object") { + settings = url; + } else { + settings = settings || {}; + settings.url = url; + } + + settings.type = settings.type || settings.method || 'get'; + settings.method = settings.type; + settings.dataFormat = settings.dataFormat || 'form'; + + if (settings.data) { + var formEncode = function (obj) { + var str = []; + for (var p in obj) { + if (obj[p] !== undefined) { + if (Array.isArray(obj[p])) { + for (var i = 0; i < obj[p].length; i++) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p][i])); + } + } else { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + } + } + + str.sort(); + + return str.join("&"); + }; + if (typeof settings.data === 'object') { + if (settings.dataFormat == 'form') { + settings.data = formEncode(settings.data); + settings.contentType = settings.contentType || 'application/x-www-form-urlencoded'; + } else if (settings.dataFormat == 'json') { + settings.data = JSON.stringify(settings.data); + settings.contentType = settings.contentType || 'application/json'; + } else { + throw new Error("Tomahawk.ajax: unknown dataFormat requested: " + + settings.dataFormat); + } + } else { + throw new Error("Tomahawk.ajax: data should be either object or string"); + } + + if (settings.type.toLowerCase() === 'get') { + settings.url += '?' + settings.data; + delete settings.data; + } else { + settings.headers = settings.headers || {}; + if (!settings.headers.hasOwnProperty('Content-Type')) { + settings.headers['Content-Type'] = settings.contentType; + } + } + } + + return doRequest(settings).then(function (xhr) { + if (settings.rawResponse) { + return xhr; + } + var responseText = xhr.responseText; + var contentType; + if (settings.dataType === 'json') { + contentType = 'application/json'; + } else if (settings.dataType === 'xml') { + contentType = 'text/xml'; + } else if (typeof xhr.getResponseHeader !== 'undefined') { + contentType = xhr.getResponseHeader('Content-Type'); + } else if (xhr.hasOwnProperty('contentType')) { + contentType = xhr['contentType']; + } else { + contentType = 'text/html'; + } + + if (~contentType.indexOf('application/json')) { + return JSON.parse(responseText); + } + + if (~contentType.indexOf('text/xml')) { + var domParser = new DOMParser(); + return domParser.parseFromString(responseText, "text/xml"); + } + + return xhr.responseText; + }); +}; + +Tomahawk.post = function (url, settings) { + if (typeof url === "object") { + settings = url; + } else { + settings = settings || {}; + settings.url = url; + } + + settings.method = 'POST'; + + return Tomahawk.ajax(settings); +}; + +Tomahawk.get = function (url, settings) { + return Tomahawk.ajax(url, settings); +}; + +Tomahawk.assert = function (assertion, message) { + Tomahawk.nativeAssert(assertion, message); +}; + +Tomahawk.sha256 = Tomahawk.sha256 || function (message) { + return CryptoJS.SHA256(message).toString(CryptoJS.enc.Hex); + }; +Tomahawk.md5 = Tomahawk.md5 || function (message) { + return CryptoJS.MD5(message).toString(CryptoJS.enc.Hex); + }; +// Return a HMAC (md5) signature of the input text with the desired key +Tomahawk.hmac = function (key, message) { + return CryptoJS.HmacMD5(message, key).toString(CryptoJS.enc.Hex); +}; + +// Extracted from https://github.com/andrewrk/diacritics version 1.2.0 +// Thanks to Andrew Kelley for this MIT-licensed diacritic removal code +// Initialisation / precomputation +(function() { + var replacementList = [ + {base: ' ', chars: "\u00A0"}, + {base: '0', chars: "\u07C0"}, + {base: 'A', chars: "\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F"}, + {base: 'AA', chars: "\uA732"}, + {base: 'AE', chars: "\u00C6\u01FC\u01E2"}, + {base: 'AO', chars: "\uA734"}, + {base: 'AU', chars: "\uA736"}, + {base: 'AV', chars: "\uA738\uA73A"}, + {base: 'AY', chars: "\uA73C"}, + {base: 'B', chars: "\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0181"}, + {base: 'C', chars: "\uFF43\u24b8\uff23\uA73E\u1E08"}, + {base: 'D', chars: "\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018A\u0189\u1D05\uA779"}, + {base: 'Dh', chars: "\u00D0"}, + {base: 'DZ', chars: "\u01F1\u01C4"}, + {base: 'Dz', chars: "\u01F2\u01C5"}, + {base: 'E', chars: "\u025B\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E\u1D07"}, + {base: 'F', chars: "\uA77C\u24BB\uFF26\u1E1E\u0191\uA77B"}, + {base: 'G', chars: "\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E\u0262"}, + {base: 'H', chars: "\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D"}, + {base: 'I', chars: "\u24BE\uFF29\xCC\xCD\xCE\u0128\u012A\u012C\u0130\xCF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197"}, + {base: 'J', chars: "\u24BF\uFF2A\u0134\u0248\u0237"}, + {base: 'K', chars: "\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2"}, + {base: 'L', chars: "\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780"}, + {base: 'LJ', chars: "\u01C7"}, + {base: 'Lj', chars: "\u01C8"}, + {base: 'M', chars: "\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C\u03FB"}, + {base: 'N', chars: "\uA7A4\u0220\u24C3\uFF2E\u01F8\u0143\xD1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u019D\uA790\u1D0E"}, + {base: 'NJ', chars: "\u01CA"}, + {base: 'Nj', chars: "\u01CB"}, + {base: 'O', chars: "\u24C4\uFF2F\xD2\xD3\xD4\u1ED2\u1ED0\u1ED6\u1ED4\xD5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\xD6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\xD8\u01FE\u0186\u019F\uA74A\uA74C"}, + {base: 'OE', chars: "\u0152"}, + {base: 'OI', chars: "\u01A2"}, + {base: 'OO', chars: "\uA74E"}, + {base: 'OU', chars: "\u0222"}, + {base: 'P', chars: "\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754"}, + {base: 'Q', chars: "\u24C6\uFF31\uA756\uA758\u024A"}, + {base: 'R', chars: "\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782"}, + {base: 'S', chars: "\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784"}, + {base: 'T', chars: "\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786"}, + {base: 'Th', chars: "\u00DE"}, + {base: 'TZ', chars: "\uA728"}, + {base: 'U', chars: "\u24CA\uFF35\xD9\xDA\xDB\u0168\u1E78\u016A\u1E7A\u016C\xDC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244"}, + {base: 'V', chars: "\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245"}, + {base: 'VY', chars: "\uA760"}, + {base: 'W', chars: "\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72"}, + {base: 'X', chars: "\u24CD\uFF38\u1E8A\u1E8C"}, + {base: 'Y', chars: "\u24CE\uFF39\u1EF2\xDD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE"}, + {base: 'Z', chars: "\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762"}, + {base: 'a', chars: "\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250\u0251"}, + {base: 'aa', chars: "\uA733"}, + {base: 'ae', chars: "\u00E6\u01FD\u01E3"}, + {base: 'ao', chars: "\uA735"}, + {base: 'au', chars: "\uA737"}, + {base: 'av', chars: "\uA739\uA73B"}, + {base: 'ay', chars: "\uA73D"}, + {base: 'b', chars: "\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253\u0182"}, + {base: 'c', chars: "\u24D2\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184\u0043\u0106\u0108\u010A\u010C\u00C7\u0187\u023B"}, + {base: 'd', chars: "\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\u018B\u13E7\u0501\uA7AA"}, + {base: 'dh', chars: "\u00F0"}, + {base: 'dz', chars: "\u01F3\u01C6"}, + {base: 'e', chars: "\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u01DD"}, + {base: 'f', chars: "\u24D5\uFF46\u1E1F\u0192"}, + {base: 'ff', chars: "\uFB00"}, + {base: 'fi', chars: "\uFB01"}, + {base: 'fl', chars: "\uFB02"}, + {base: 'ffi', chars: "\uFB03"}, + {base: 'ffl', chars: "\uFB04"}, + {base: 'g', chars: "\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\uA77F\u1D79"}, + {base: 'h', chars: "\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265"}, + {base: 'hv', chars: "\u0195"}, + {base: 'i', chars: "\u24D8\uFF49\xEC\xED\xEE\u0129\u012B\u012D\xEF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131"}, + {base: 'j', chars: "\u24D9\uFF4A\u0135\u01F0\u0249"}, + {base: 'k', chars: "\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3"}, + {base: 'l', chars: "\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747\u026D"}, + {base: 'lj', chars: "\u01C9"}, + {base: 'm', chars: "\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F"}, + {base: 'n', chars: "\u24DD\uFF4E\u01F9\u0144\xF1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5\u043B\u0509"}, + {base: 'nj', chars: "\u01CC"}, + {base: 'o', chars: "\u24DE\uFF4F\xF2\xF3\xF4\u1ED3\u1ED1\u1ED7\u1ED5\xF5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\xF6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\xF8\u01FF\uA74B\uA74D\u0275\u0254\u1D11"}, + {base: 'oe', chars: "\u0153"}, + {base: 'oi', chars: "\u01A3"}, + {base: 'oo', chars: "\uA74F"}, + {base: 'ou', chars: "\u0223"}, + {base: 'p', chars: "\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755\u03C1"}, + {base: 'q', chars: "\u24E0\uFF51\u024B\uA757\uA759"}, + {base: 'r', chars: "\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783"}, + {base: 's', chars: "\u24E2\uFF53\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B\u0282"}, + {base: 'ss', chars: "\xDF"}, + {base: 't', chars: "\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787"}, + {base: 'th', chars: "\u00FE"}, + {base: 'tz', chars: "\uA729"}, + {base: 'u', chars: "\u24E4\uFF55\xF9\xFA\xFB\u0169\u1E79\u016B\u1E7B\u016D\xFC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289"}, + {base: 'v', chars: "\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C"}, + {base: 'vy', chars: "\uA761"}, + {base: 'w', chars: "\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73"}, + {base: 'x', chars: "\u24E7\uFF58\u1E8B\u1E8D"}, + {base: 'y', chars: "\u24E8\uFF59\u1EF3\xFD\u0177\u1EF9\u0233\u1E8F\xFF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF"}, + {base: 'z', chars: "\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763"} + ]; + + Tomahawk.diacriticsMap = {}; + var i, j, chars; + for (i = 0; i < replacementList.length; i += 1) { + chars = replacementList[i].chars; + for (j = 0; j < chars.length; j += 1) { + Tomahawk.diacriticsMap[chars[j]] = replacementList[i].base; + } + } +})(); + +Tomahawk.removeDiacritics = function (str) { + return str.replace(/[^\u0000-\u007E]/g, function (c) { + return Tomahawk.diacriticsMap[c] || c; + }); +}; + +Tomahawk.localStorage = Tomahawk.localStorage || { + setItem: function (key, value) { + window.localStorage[key] = value; + }, + getItem: function (key) { + return window.localStorage[key]; + }, + removeItem: function (key) { + delete window.localStorage[key]; + } + }; + +// some aliases +Tomahawk.setTimeout = Tomahawk.setTimeout || window.setTimeout; +Tomahawk.setInterval = Tomahawk.setInterval || window.setInterval; +Tomahawk.base64Decode = function (a) { + return window.atob(a); +}; +Tomahawk.base64Encode = function (b) { + return window.btoa(b); +}; + +Tomahawk.PluginManager = { + wrapperPrefix: '_adapter_', + objects: {}, + objectCounter: 0, + identifyObject: function (object) { + if (!object.hasOwnProperty('id')) { + object.id = this.objectCounter++; + } + + return object.id; + }, + registerPlugin: function (type, object) { + this.objects[this.identifyObject(object)] = object; + Tomahawk.log("registerPlugin: " + type + " id: " + object.id); + Tomahawk.registerScriptPlugin(type, object.id); + }, + + unregisterPlugin: function (type, object) { + this.objects[this.identifyObject(object)] = object; + + Tomahawk.log("unregisterPlugin: " + type + " id: " + object.id); + Tomahawk.unregisterScriptPlugin(type, object.id); + }, + + resolve: [], + invokeSync: function (requestId, objectId, methodName, params) { + if (this.objects[objectId][this.wrapperPrefix + methodName]) { + methodName = this.wrapperPrefix + methodName; + } + + if (!this.objects[objectId]) { + Tomahawk.log("Object not found! objectId: " + objectId + " methodName: " + methodName); + } else { + if (!this.objects[objectId][methodName]) { + Tomahawk.log("Function not found: " + methodName); + } + } + + if (typeof this.objects[objectId][methodName] !== 'function' && this.objects[objectId][methodName]) { + return this.objects[objectId][methodName]; + } else if (typeof this.objects[objectId][methodName] !== 'function') { + throw new Error('\'' + methodName + '\' on ScriptObject ' + objectId + ' is not a function', typeof this.objects[objectId][methodName]); + } + + return this.objects[objectId][methodName](params); + }, + + invoke: function (requestId, objectId, methodName, params) { + RSVP.Promise.resolve(this.invokeSync(requestId, objectId, methodName, params)) + .then(function (result) { + var params = { + requestId: requestId, + data: result + }; + Tomahawk.reportScriptJobResults(encodeParamsToNativeFunctions(params)); + }, function (error) { + var params = { + requestId: requestId, + error: error + }; + Tomahawk.reportScriptJobResults(encodeParamsToNativeFunctions(params)); + }); + } +}; + +var encodeParamsToNativeFunctions = function(param) { + return param; +}; + +Tomahawk.NativeScriptJobManager = { + idCounter: 0, + deferreds: {}, + invoke: function (methodName, params) { + params = params || {}; + + var requestId = this.idCounter++; + var deferred = RSVP.defer(); + this.deferreds[requestId] = deferred; + Tomahawk.invokeNativeScriptJob(requestId, methodName, encodeParamsToNativeFunctions(params)); + return deferred.promise; + }, + reportNativeScriptJobResult: function(requestId, result) { + var deferred = this.deferreds[requestId]; + if (!deferred) { + Tomahawk.log("Deferred object with the given requestId is not present!"); + } + deferred.resolve(result); + delete this.deferreds[requestId]; + }, + reportNativeScriptJobError: function(requestId, error) { + var deferred = this.deferreds[requestId]; + if (!deferred) { + console.log("Deferred object with the given requestId is not present!"); + } + deferred.reject(error); + delete this.deferreds[requestId]; + } +}; + +Tomahawk.UrlType = { + Any: 0, + Playlist: 1, + Track: 2, + Album: 3, + Artist: 4, + XspfPlaylist: 5 +}; + +Tomahawk.ConfigTestResultType = { + Other: 0, + Success: 1, + Logout: 2, + CommunicationError: 3, + InvalidCredentials: 4, + InvalidAccount: 5, + PlayingElsewhere: 6, + AccountExpired: 7 +}; + +Tomahawk.Country = { + AnyCountry: 0, + Afghanistan: 1, + Albania: 2, + Algeria: 3, + AmericanSamoa: 4, + Andorra: 5, + Angola: 6, + Anguilla: 7, + Antarctica: 8, + AntiguaAndBarbuda: 9, + Argentina: 10, + Armenia: 11, + Aruba: 12, + Australia: 13, + Austria: 14, + Azerbaijan: 15, + Bahamas: 16, + Bahrain: 17, + Bangladesh: 18, + Barbados: 19, + Belarus: 20, + Belgium: 21, + Belize: 22, + Benin: 23, + Bermuda: 24, + Bhutan: 25, + Bolivia: 26, + BosniaAndHerzegowina: 27, + Botswana: 28, + BouvetIsland: 29, + Brazil: 30, + BritishIndianOceanTerritory: 31, + BruneiDarussalam: 32, + Bulgaria: 33, + BurkinaFaso: 34, + Burundi: 35, + Cambodia: 36, + Cameroon: 37, + Canada: 38, + CapeVerde: 39, + CaymanIslands: 40, + CentralAfricanRepublic: 41, + Chad: 42, + Chile: 43, + China: 44, + ChristmasIsland: 45, + CocosIslands: 46, + Colombia: 47, + Comoros: 48, + DemocraticRepublicOfCongo: 49, + PeoplesRepublicOfCongo: 50, + CookIslands: 51, + CostaRica: 52, + IvoryCoast: 53, + Croatia: 54, + Cuba: 55, + Cyprus: 56, + CzechRepublic: 57, + Denmark: 58, + Djibouti: 59, + Dominica: 60, + DominicanRepublic: 61, + EastTimor: 62, + Ecuador: 63, + Egypt: 64, + ElSalvador: 65, + EquatorialGuinea: 66, + Eritrea: 67, + Estonia: 68, + Ethiopia: 69, + FalklandIslands: 70, + FaroeIslands: 71, + FijiCountry: 72, + Finland: 73, + France: 74, + MetropolitanFrance: 75, + FrenchGuiana: 76, + FrenchPolynesia: 77, + FrenchSouthernTerritories: 78, + Gabon: 79, + Gambia: 80, + Georgia: 81, + Germany: 82, + Ghana: 83, + Gibraltar: 84, + Greece: 85, + Greenland: 86, + Grenada: 87, + Guadeloupe: 88, + Guam: 89, + Guatemala: 90, + Guinea: 91, + GuineaBissau: 92, + Guyana: 93, + Haiti: 94, + HeardAndMcDonaldIslands: 95, + Honduras: 96, + HongKong: 97, + Hungary: 98, + Iceland: 99, + India: 100, + Indonesia: 101, + Iran: 102, + Iraq: 103, + Ireland: 104, + Israel: 105, + Italy: 106, + Jamaica: 107, + Japan: 108, + Jordan: 109, + Kazakhstan: 110, + Kenya: 111, + Kiribati: 112, + DemocraticRepublicOfKorea: 113, + RepublicOfKorea: 114, + Kuwait: 115, + Kyrgyzstan: 116, + Lao: 117, + Latvia: 118, + Lebanon: 119, + Lesotho: 120, + Liberia: 121, + LibyanArabJamahiriya: 122, + Liechtenstein: 123, + Lithuania: 124, + Luxembourg: 125, + Macau: 126, + Macedonia: 127, + Madagascar: 128, + Malawi: 129, + Malaysia: 130, + Maldives: 131, + Mali: 132, + Malta: 133, + MarshallIslands: 134, + Martinique: 135, + Mauritania: 136, + Mauritius: 137, + Mayotte: 138, + Mexico: 139, + Micronesia: 140, + Moldova: 141, + Monaco: 142, + Mongolia: 143, + Montserrat: 144, + Morocco: 145, + Mozambique: 146, + Myanmar: 147, + Namibia: 148, + NauruCountry: 149, + Nepal: 150, + Netherlands: 151, + NetherlandsAntilles: 152, + NewCaledonia: 153, + NewZealand: 154, + Nicaragua: 155, + Niger: 156, + Nigeria: 157, + Niue: 158, + NorfolkIsland: 159, + NorthernMarianaIslands: 160, + Norway: 161, + Oman: 162, + Pakistan: 163, + Palau: 164, + PalestinianTerritory: 165, + Panama: 166, + PapuaNewGuinea: 167, + Paraguay: 168, + Peru: 169, + Philippines: 170, + Pitcairn: 171, + Poland: 172, + Portugal: 173, + PuertoRico: 174, + Qatar: 175, + Reunion: 176, + Romania: 177, + RussianFederation: 178, + Rwanda: 179, + SaintKittsAndNevis: 180, + StLucia: 181, + StVincentAndTheGrenadines: 182, + Samoa: 183, + SanMarino: 184, + SaoTomeAndPrincipe: 185, + SaudiArabia: 186, + Senegal: 187, + SerbiaAndMontenegro: 241, + Seychelles: 188, + SierraLeone: 189, + Singapore: 190, + Slovakia: 191, + Slovenia: 192, + SolomonIslands: 193, + Somalia: 194, + SouthAfrica: 195, + SouthGeorgiaAndTheSouthSandwichIslands: 196, + Spain: 197, + SriLanka: 198, + StHelena: 199, + StPierreAndMiquelon: 200, + Sudan: 201, + Suriname: 202, + SvalbardAndJanMayenIslands: 203, + Swaziland: 204, + Sweden: 205, + Switzerland: 206, + SyrianArabRepublic: 207, + Taiwan: 208, + Tajikistan: 209, + Tanzania: 210, + Thailand: 211, + Togo: 212, + Tokelau: 213, + TongaCountry: 214, + TrinidadAndTobago: 215, + Tunisia: 216, + Turkey: 217, + Turkmenistan: 218, + TurksAndCaicosIslands: 219, + Tuvalu: 220, + Uganda: 221, + Ukraine: 222, + UnitedArabEmirates: 223, + UnitedKingdom: 224, + UnitedStates: 225, + UnitedStatesMinorOutlyingIslands: 226, + Uruguay: 227, + Uzbekistan: 228, + Vanuatu: 229, + VaticanCityState: 230, + Venezuela: 231, + VietNam: 232, + BritishVirginIslands: 233, + USVirginIslands: 234, + WallisAndFutunaIslands: 235, + WesternSahara: 236, + Yemen: 237, + Yugoslavia: 238, + Zambia: 239, + Zimbabwe: 240, + Montenegro: 242, + Serbia: 243, + SaintBarthelemy: 244, + SaintMartin: 245, + LatinAmericaAndTheCaribbean: 246 +}; + +Tomahawk.Collection = { + BrowseCapability: { + Artists: 1, + Albums: 2, + Tracks: 4 + }, + + cachedDbs: {}, + + Transaction: function (collection, id) { + + this.ensureDb = function () { + return new RSVP.Promise(function (resolve, reject) { + if (!collection.cachedDbs.hasOwnProperty(id)) { + Tomahawk.log("Opening database"); + var estimatedSize = 5 * 1024 * 1024; // 5MB + collection.cachedDbs[id] = + openDatabase(id + "_collection", "", "Collection", estimatedSize); + + collection.cachedDbs[id].transaction(function (tx) { + Tomahawk.log("Creating initial db tables"); + tx.executeSql("CREATE TABLE IF NOT EXISTS artists(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "artist TEXT ," + + "artistDisambiguation TEXT," + + "UNIQUE (artist, artistDisambiguation) ON CONFLICT IGNORE)", []); + tx.executeSql("CREATE TABLE IF NOT EXISTS albumArtists(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "albumArtist TEXT ," + + "albumArtistDisambiguation TEXT," + + "UNIQUE (albumArtist, albumArtistDisambiguation) ON CONFLICT IGNORE)", + []); + tx.executeSql("CREATE TABLE IF NOT EXISTS albums(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "album TEXT," + + "albumArtistId INTEGER," + + "UNIQUE (album, albumArtistId) ON CONFLICT IGNORE," + + "FOREIGN KEY(albumArtistId) REFERENCES albumArtists(_id))", []); + tx.executeSql("CREATE TABLE IF NOT EXISTS artistAlbums(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "albumId INTEGER," + + "artistId INTEGER," + + "UNIQUE (albumId, artistId) ON CONFLICT IGNORE," + + "FOREIGN KEY(albumId) REFERENCES albums(_id)," + + "FOREIGN KEY(artistId) REFERENCES artists(_id))", []); + tx.executeSql("CREATE TABLE IF NOT EXISTS tracks(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "track TEXT," + + "artistId INTEGER," + + "albumId INTEGER," + + "url TEXT," + + "duration INTEGER," + + "albumPos INTEGER," + + "linkUrl TEXT," + + 'releaseyear INTEGER,' + + 'bitrate INTEGER,' + + "UNIQUE (track, artistId, albumId) ON CONFLICT IGNORE," + + "FOREIGN KEY(artistId) REFERENCES artists(_id)," + + "FOREIGN KEY(albumId) REFERENCES albums(_id))", []); + }); + } + resolve(collection.cachedDbs[id]); + }); + }; + + this.beginTransaction = function () { + var that = this; + return this.ensureDb().then(function (db) { + return new RSVP.Promise(function (resolve, reject) { + that.db = db; + that.statements = []; + resolve(); + }) + }); + }; + + this.execDeferredStatements = function (resolve, reject) { + var that = this; + that.stmtsToResolve = that.statements.length; + that.results = that.statements.slice(); + Tomahawk.log('Executing ' + that.stmtsToResolve + + ' deferred SQL statements in transaction'); + return new RSVP.Promise(function (resolve, reject) { + if (that.statements.length == 0) { + resolve([]); + } else { + that.db.transaction(function (tx) { + for (var i = 0; i < that.statements.length; ++i) { + var stmt = that.statements[i]; + tx.executeSql(stmt.statement, stmt.args, + (function () { + //A function returning a function to + //capture value of i + var originalI = i; + return function (tx, results) { + if (typeof that.statements[originalI].map !== 'undefined') { + var map = that.statements[originalI].map; + that.results[originalI] = []; + for (var ii = 0; ii < results.rows.length; ii++) { + that.results[originalI].push(map( + results.rows.item(ii) + )); + } + } + else { + that.results[originalI] = results; + } + that.stmtsToResolve--; + if (that.stmtsToResolve == 0) { + that.statements = []; + resolve(that.results); + } + }; + })(), function (tx, error) { + Tomahawk.log("Error in tx.executeSql: " + error.code + " - " + + error.message); + that.statements = []; + reject(error); + } + ); + } + }); + } + }); + }; + + this.sql = function (sqlStatement, sqlArgs, mapFunction) { + this.statements.push({statement: sqlStatement, args: sqlArgs, map: mapFunction}); + }; + + this.sqlSelect = function (table, mapResults, fields, where, join) { + var whereKeys = []; + var whereValues = []; + for (var whereKey in where) { + if (where.hasOwnProperty(whereKey)) { + whereKeys.push(table + "." + whereKey + " = ?"); + whereValues.push(where[whereKey]); + } + } + var whereString = whereKeys.length > 0 ? " WHERE " + whereKeys.join(" AND ") : ""; + + var joinString = ""; + for (var i = 0; join && i < join.length; i++) { + var joinConditions = []; + for (var joinKey in join[i].conditions) { + if (join[i].conditions.hasOwnProperty(joinKey)) { + joinConditions.push(table + "." + joinKey + " = " + join[i].table + "." + + join[i].conditions[joinKey]); + } + } + joinString += " INNER JOIN " + join[i].table + " ON " + + joinConditions.join(" AND "); + } + + var fieldsString = fields && fields.length > 0 ? fields.join(", ") : "*"; + var statement = "SELECT " + fieldsString + " FROM " + table + joinString + whereString; + return this.sql(statement, whereValues, mapResults); + }; + + this.sqlInsert = function (table, fields) { + var fieldsKeys = []; + var fieldsValues = []; + var valuesString = ""; + for (var key in fields) { + fieldsKeys.push(key); + fieldsValues.push(fields[key]); + if (valuesString.length > 0) { + valuesString += ", "; + } + valuesString += "?"; + } + var statement = "INSERT INTO " + table + " (" + fieldsKeys.join(", ") + ") VALUES (" + + valuesString + ")"; + return this.sql(statement, fieldsValues); + }; + + this.sqlDrop = function (table) { + var statement = "DROP TABLE IF EXISTS " + table; + return this.sql(statement, []); + }; + + }, + + addTracks: function (params) { + var that = this; + var id = params.id; + var tracks = params.tracks; + + var cachedAlbumArtists = {}, + cachedArtists = {}, + cachedAlbums = {}, + cachedArtistIds = {}, + cachedAlbumIds = {}; + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + // First we insert all artists and albumArtists + t.sqlInsert("artists", { + artist: "Various Artists", + artistDisambiguation: "" + }); + for (var i = 0; i < tracks.length; i++) { + tracks[i].track = tracks[i].track || ""; + tracks[i].album = tracks[i].album || ""; + tracks[i].artist = tracks[i].artist || ""; + tracks[i].artistDisambiguation = tracks[i].artistDisambiguation || ""; + tracks[i].albumArtist = tracks[i].albumArtist || ""; + tracks[i].albumArtistDisambiguation = tracks[i].albumArtistDisambiguation || ""; + (function (track) { + t.sqlInsert("artists", { + artist: track.artist, + artistDisambiguation: track.artistDisambiguation + }); + t.sqlInsert("albumArtists", { + albumArtist: track.albumArtist, + albumArtistDisambiguation: track.albumArtistDisambiguation + }); + })(tracks[i]); + } + return t.execDeferredStatements(); + }).then(function () { + // Get all artists' and albumArtists' db ids + t.sqlSelect("albumArtists", function (r) { + return { + albumArtist: r.albumArtist, + albumArtistDisambiguation: r.albumArtistDisambiguation, + _id: r._id + }; + }); + t.sqlSelect("artists", function (r) { + return { + artist: r.artist, + artistDisambiguation: r.artistDisambiguation, + _id: r._id + }; + }); + return t.execDeferredStatements(); + }).then(function (resultsArray) { + // Store the db ids in a map + var i, row, albumArtists = {}; + for (i = 0; i < resultsArray[0].length; i++) { + row = resultsArray[0][i]; + albumArtists[row.albumArtist + "♣" + row.albumArtistDisambiguation] = row._id; + } + for (i = 0; i < resultsArray[1].length; i++) { + row = resultsArray[1][i]; + cachedArtists[row.artist + "♣" + row.artistDisambiguation] = row._id; + cachedArtistIds[row._id] = { + artist: row.artist, + artistDisambiguation: row.artistDisambiguation + }; + } + + for (i = 0; i < tracks.length; i++) { + var track = tracks[i]; + var album = cachedAlbumArtists[track.album]; + if (!album) { + album = cachedAlbumArtists[track.album] = { + artists: {} + }; + } + album.artists[track.artist] = true; + var artistCount = Object.keys(album.artists).length; + if (artistCount == 1) { + album.albumArtistId = + cachedArtists[track.artist + "♣" + track.artistDisambiguation]; + } else if (artistCount == 2) { + album.albumArtistId = cachedArtists["Various Artists♣"]; + } + } + }).then(function () { + // Insert all albums + for (var i = 0; i < tracks.length; i++) { + (function (track) { + var albumArtistId = cachedAlbumArtists[track.album].albumArtistId; + t.sqlInsert("albums", { + album: track.album, + albumArtistId: albumArtistId + }); + })(tracks[i]); + } + return t.execDeferredStatements(); + }).then(function () { + // Get the albums' db ids + t.sqlSelect("albums", function (r) { + return { + album: r.album, + albumArtistId: r.albumArtistId, + _id: r._id + }; + }); + return t.execDeferredStatements(); + }).then(function (results) { + // Store the db ids in a map + results = results[0]; + for (var i = 0; i < results.length; i++) { + var row = results[i]; + cachedAlbums[row.album + "♣" + row.albumArtistId] = row._id; + cachedAlbumIds[row._id] = { + album: row.album, + albumArtistId: row.albumArtistId + }; + } + }).then(function () { + // Now we are ready to insert the tracks + for (var i = 0; i < tracks.length; i++) { + (function (track) { + // Get all relevant ids that we stored in the previous steps + var artistId = cachedArtists[track.artist + "♣" + track.artistDisambiguation]; + var albumArtistId = cachedAlbumArtists[track.album].albumArtistId; + var albumId = cachedAlbums[track.album + "♣" + albumArtistId]; + // Insert the artist <=> album relations + t.sqlInsert("artistAlbums", { + albumId: albumId, + artistId: artistId + }); + // Insert the tracks + t.sqlInsert("tracks", { + track: track.track, + artistId: artistId, + albumId: albumId, + url: track.url, + duration: track.duration, + linkUrl: track.linkUrl, + releaseyear: track.releaseyear, + bitrate: track.bitrate, + albumPos: track.albumpos + }); + })(tracks[i]); + } + return t.execDeferredStatements(); + }).then(function () { + var resultMap = function (r) { + return { + _id: r._id, + artistId: r.artistId, + albumId: r.albumId, + track: r.track + }; + }; + // Get the tracks' db ids + t.sqlSelect("tracks", resultMap, ["_id", "artistId", "albumId", "track"]); + return t.execDeferredStatements(); + }).then(function (results) { + that._trackCount = results[0].length; + Tomahawk.log("Added " + results[0].length + " tracks to collection '" + id + "'"); + // Add the db ids together with the basic metadata to the fuzzy index list + var fuzzyIndexList = []; + for (var i = 0; i < results[0].length; i++) { + var row = results[0][i]; + fuzzyIndexList.push({ + id: row._id, + artist: cachedArtistIds[row.artistId].artist, + album: cachedAlbumIds[row.albumId].album, + track: row.track + }); + } + Tomahawk.createFuzzyIndex(fuzzyIndexList); + }); + }, + + wipe: function (params) { + var id = params.id; + + var that = this; + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + t.sqlDrop("artists"); + t.sqlDrop("albumArtists"); + t.sqlDrop("albums"); + t.sqlDrop("artistAlbums"); + t.sqlDrop("tracks"); + return t.execDeferredStatements(); + }).then(function () { + return new RSVP.Promise(function (resolve, reject) { + that.cachedDbs[id].changeVersion(that.cachedDbs[id].version, "", null, + function (err) { + Tomahawk.error("Error trying to change db version!", err); + reject(); + }, function () { + delete that.cachedDbs[id]; + Tomahawk.deleteFuzzyIndex(id); + Tomahawk.log("Wiped collection '" + id + "'"); + resolve(); + }); + }); + }); + }, + + revision: function (params) { + return RSVP.resolve(); + }, + + _fuzzyIndexIdsToTracks: function (resultIds, id) { + if (typeof id === 'undefined') { + id = this.settings.id; + } + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + var mapFn = function (row) { + return { + artist: row.artist, + artistDisambiguation: row.artistDisambiguation, + album: row.album, + track: row.track, + duration: row.duration, + url: row.url, + linkUrl: row.linkUrl, + releaseyear: row.releaseyear, + bitrate: row.bitrate, + albumpos: row.albumPos + }; + }; + for (var idx = 0; resultIds && idx < resultIds.length; idx++) { + var trackid = resultIds[idx][0]; + var where = {_id: trackid}; + t.sqlSelect("tracks", mapFn, + [], + where, [ + { + table: "artists", + conditions: { + artistId: "_id" + } + }, { + table: "albums", + conditions: { + albumId: "_id" + } + } + ] + ); + } + return t.execDeferredStatements(); + }).then(function (results) { + var merged = []; + return merged.concat.apply(merged, + results.map(function (e) { + //every result has one track + return e[0]; + })); + }); + }, + + _adapter_resolve: function (params) { + return RSVP.Promise.resolve(this.resolve(params)).then(function (results) { + return { + 'tracks': results + }; + }); + }, + + resolve: function (params) { + var resultIds = Tomahawk.resolveFromFuzzyIndex(params.artist, params.album, params.track); + return this._fuzzyIndexIdsToTracks(resultIds); + }, + + search: function (params) { + var resultIds = Tomahawk.searchFuzzyIndex(params.query); + + return this._fuzzyIndexIdsToTracks(resultIds).then(function(tracks) { + return { + tracks: tracks + }; + }); + }, + + tracks: function (params, where) { + //TODO filter/where support + var id = params.id; + if (typeof id === 'undefined') { + id = this.settings.id; + } + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + var mapFn = function (row) { + return { + artist: row.artist, + artistDisambiguation: row.artistDisambiguation, + album: row.album, + track: row.track, + duration: row.duration, + url: row.url, + linkUrl: row.linkUrl, + releaseyear: row.releaseyear, + bitrate: row.bitrate, + albumpos: row.albumPos + }; + }; + t.sqlSelect("tracks", mapFn, + [], + where, [ + { + table: "artists", + conditions: { + artistId: "_id" + } + }, { + table: "albums", + conditions: { + albumId: "_id" + } + } + ] + ); + return t.execDeferredStatements(); + }).then(function (results) { + return {tracks: results[0]}; + }); + }, + + albums: function (params, where) { + //TODO filter/where support + var id = params.id; + if (typeof id === 'undefined') { + id = this.settings.id; + } + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + var mapFn = function (row) { + return { + albumArtist: row.artist, + albumArtistDisambiguation: row.artistDisambiguation, + album: row.album + }; + }; + t.sqlSelect("albums", mapFn, + ["album", "artist", "artistDisambiguation"], + where, [ + { + table: "artists", + conditions: { + albumArtistId: "_id" + } + } + ] + ); + return t.execDeferredStatements(); + }).then(function (results) { + results = results[0].filter(function (e) { + return (e.albumArtist != '' && e.album != ''); + }); + return { + artists: results.map(function (i) { + return i.albumArtist; + }), + albums: results.map(function (i) { + return i.album; + }) + }; + }); + }, + + artists: function (params) { + //TODO filter/where support + var id = params.id; + if (typeof id === 'undefined') { + id = this.settings.id; + } + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + var mapFn = function (r) { + return r.artist; + }; + t.sqlSelect("artists", mapFn, ["artist", "artistDisambiguation"]); + return t.execDeferredStatements(); + }).then(function (artists) { + return {artists: artists[0]}; + }); + }, + + //TODO: not exactly sure how is this one supposed to work + //albumArtists: function (params) { + //var id = params.id; + + //var t = new Tomahawk.Collection.Transaction(this, id); + //return t.beginTransaction().then(function () { + //var mapFn = function(row) { + //return { + //albumArtist: row.albumArtist, + //albumArtistDisambiguation: row.albumArtistDisambiguation + //}; + //}; + //t.sqlSelect("albumArtists", ["albumArtist", "albumArtistDisambiguation"]); + //return t.execDeferredStatements(); + //}).then(function (results) { + //return results[0]; + //}); + //}, + + artistAlbums: function (params) { + //TODO filter/where support + var id = params.id; + if (typeof id === 'undefined') { + id = this.settings.id; + } + var artist = params.artist; + //var artistDisambiguation = params.artistDisambiguation; + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + + t.sqlSelect("artists", function (r) { + return r._id; + }, ["_id"], { + artist: artist + //artistDisambiguation: artistDisambiguation + }); + return t.execDeferredStatements(); + }).then(function (results) { + var artistId = results[0][0]; + t.sqlSelect("artistAlbums", function (r) { + return r.album; + }, ["albumId", 'album'], { + artistId: artistId + }, [ + { + table: "albums", + conditions: { + albumId: "_id" + } + } + ]); + return t.execDeferredStatements(); + }).then(function (results) { + return { + artist: artist, + albums: results[0] + }; + }); + }, + + albumTracks: function (params) { + //TODO filter/where support + var id = params.id; + if (typeof id === 'undefined') { + id = this.settings.id; + } + var albumArtist = params.artist; + //var albumArtistDisambiguation = params.albumArtistDisambiguation; + var album = params.album; + + var that = this; + + var t = new Tomahawk.Collection.Transaction(this, id); + return t.beginTransaction().then(function () { + t.sqlSelect("artists", function (r) { + return r._id; + }, ["_id"], { + artist: albumArtist + //artistDisambiguation: albumArtistDisambiguation + }); + return t.execDeferredStatements(); + }).then(function (results) { + var albumArtistId = results[0][0]; + t.sqlSelect("albums", function (r) { + return r._id; + }, ["_id"], { + album: album, + albumArtistId: albumArtistId + }); + return t.execDeferredStatements(); + }).then(function (results) { + var albumId = results[0][0]; + return that.tracks(params, { + albumId: albumId + }); + }); + }, + + collection: function () { + this.settings.trackcount = this._trackCount; + if (!this.settings.description) { + this.settings.description = this.settings.prettyname; + } + this.settings.capabilities = [Tomahawk.Collection.BrowseCapability.Artists, + Tomahawk.Collection.BrowseCapability.Albums, + Tomahawk.Collection.BrowseCapability.Tracks]; + return this.settings; + }, + + getStreamUrl: function(params) { + if(this.resolver) { + return this.resolver.getStreamUrl(params); + } + + return params; + } +}; diff --git a/app/src/main/assets/js/tomahawk_android_post.js b/app/src/main/assets/js/tomahawk_android_post.js new file mode 100644 index 000000000..72b96e849 --- /dev/null +++ b/app/src/main/assets/js/tomahawk_android_post.js @@ -0,0 +1,68 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * 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. + */ + +/** + * This method is externalized from Tomahawk.asyncRequest, so that other clients + * (like tomahawk-android) can inject their own logic that determines whether or not to do a request + * natively. + * + * @returns boolean indicating whether or not to do a request with the given parameters natively + */ +var shouldDoNativeRequest = function (options) { + return ((options && options.needCookieHeader) + || (options.headers + && (options.headers.hasOwnProperty("Referer") + || options.headers.hasOwnProperty("referer") + || options.headers.hasOwnProperty("Origin") + || options.headers.hasOwnProperty("origin") + || options.headers.hasOwnProperty("User-Agent")) + ) + ); +}; + +Tomahawk.Collection.addTracks = function (params) { + return Tomahawk.NativeScriptJobManager.invoke("collectionAddTracks", params); +}; + +Tomahawk.Collection.wipe = function (params) { + return Tomahawk.NativeScriptJobManager.invoke("collectionWipe", params); +}; + +Tomahawk.Collection.revision = function (params) { + return Tomahawk.NativeScriptJobManager.invoke("collectionRevision", params); +}; + +var encodeParamsToNativeFunctions = function (param) { + return JSON.stringify(param); +}; + +Tomahawk.error = function (msg, obj) { + var objString = ""; + if (obj) { + objString = " - " + JSON.stringify(obj); + } + var error; + if (obj instanceof Error) { + error = obj; + } else { + error = new Error(''); + } + var stack = error.stack; + stack = stack.split('\n').map(function (line) { + return line.trim(); + }); + stack = stack.splice(stack[0] == 'Error' ? 2 : 1).join("\n"); + console.error(msg + objString + "\n" + stack + "\n"); +}; \ No newline at end of file diff --git a/app/src/main/assets/js/tomahawk_android_pre.js b/app/src/main/assets/js/tomahawk_android_pre.js new file mode 100644 index 000000000..9e2fa2515 --- /dev/null +++ b/app/src/main/assets/js/tomahawk_android_pre.js @@ -0,0 +1,34 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * 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. + */ + +// This detour is needed because we can only send Strings and no JSON objects from Java to JS +// through the Javascript Interface. + +Tomahawk.resolverData = + function () { + return JSON.parse(Tomahawk.resolverDataString()); + }; + +Tomahawk.localStorage = { + setItem: function (key, value) { + Tomahawk.localStorageSetItem(key, value); + }, + getItem: function (key) { + return Tomahawk.localStorageGetItem(key); + }, + removeItem: function (key) { + Tomahawk.localStorageRemoveItem(key); + } +}; diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorManager.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorManager.java new file mode 100644 index 000000000..69f1b7d0a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorManager.java @@ -0,0 +1,121 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import java.util.HashMap; + +public class AuthenticatorManager { + + public final static int CONFIG_TEST_RESULT_TYPE_OTHER = 0; + + public final static int CONFIG_TEST_RESULT_TYPE_SUCCESS = 1; + + public final static int CONFIG_TEST_RESULT_TYPE_LOGOUT = 2; + + public final static int CONFIG_TEST_RESULT_TYPE_COMMERROR = 3; + + public final static int CONFIG_TEST_RESULT_TYPE_INVALIDCREDS = 4; + + public final static int CONFIG_TEST_RESULT_TYPE_INVALIDACCOUNT = 5; + + public final static int CONFIG_TEST_RESULT_TYPE_PLAYINGELSEWHERE = 6; + + public final static int CONFIG_TEST_RESULT_TYPE_ACCOUNTEXPIRED = 7; + + private static class Holder { + + private static final AuthenticatorManager instance = new AuthenticatorManager(); + + } + + public static class ConfigTestResultEvent { + + public Object mComponent; + + public int mType; + + public String mMessage; + } + + private final HashMap mAuthenticatorUtils + = new HashMap<>(); + + private AuthenticatorManager() { + HatchetAuthenticatorUtils hatchetAuthenticatorUtils = new HatchetAuthenticatorUtils(); + mAuthenticatorUtils.put(hatchetAuthenticatorUtils.getId(), + hatchetAuthenticatorUtils); + } + + public static AuthenticatorManager get() { + return Holder.instance; + } + + public AuthenticatorUtils getAuthenticatorUtils(String authenticatorId) { + return mAuthenticatorUtils.get(authenticatorId); + } + + public static void showToast(String componentName, ConfigTestResultEvent event) { + String message; + switch (event.mType) { + case CONFIG_TEST_RESULT_TYPE_SUCCESS: + message = TomahawkApp.getContext() + .getString(R.string.auth_logged_in, componentName); + break; + case CONFIG_TEST_RESULT_TYPE_LOGOUT: + message = TomahawkApp.getContext() + .getString(R.string.auth_logged_out, componentName); + break; + case CONFIG_TEST_RESULT_TYPE_INVALIDCREDS: + message = componentName + ": " + TomahawkApp.getContext().getString( + R.string.error_invalid_credentials); + break; + case CONFIG_TEST_RESULT_TYPE_INVALIDACCOUNT: + message = componentName + ": " + TomahawkApp.getContext().getString( + R.string.error_invalid_account); + break; + case CONFIG_TEST_RESULT_TYPE_COMMERROR: + message = componentName + ": " + TomahawkApp.getContext().getString( + R.string.error_communication); + break; + case CONFIG_TEST_RESULT_TYPE_PLAYINGELSEWHERE: + message = componentName + ": " + TomahawkApp.getContext().getString( + R.string.error_simultaneous_streams); + break; + case CONFIG_TEST_RESULT_TYPE_ACCOUNTEXPIRED: + message = componentName + ": " + TomahawkApp.getContext().getString( + R.string.error_account_expired); + break; + default: + message = componentName + ": " + event.mMessage; + } + final String msg = message; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(TomahawkApp.getContext(), msg, Toast.LENGTH_LONG).show(); + } + }); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorUtils.java new file mode 100644 index 000000000..9738a6d62 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/AuthenticatorUtils.java @@ -0,0 +1,56 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication; + +public abstract class AuthenticatorUtils { + + private final String mPrettyName; + + private final String mId; + + protected AuthenticatorUtils(String id, String prettyName) { + mId = id; + mPrettyName = prettyName; + } + + public String getPrettyName() { + return mPrettyName; + } + + public abstract String getDescription(); + + public String getId() { + return mId; + } + + public abstract int getIconResourceId(); + + public abstract int getUserIdEditTextHintResId(); + + public abstract void register(String name, String password, String email); + + public abstract void login(String email, String password); + + public abstract void logout(); + + public abstract boolean isLoggedIn(); + + public abstract String getUserName(); + + public abstract boolean doesAllowRegistration(); +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuth.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuth.java new file mode 100644 index 000000000..b7f22ba8e --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuth.java @@ -0,0 +1,60 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication; + +import org.tomahawk.libtomahawk.authentication.models.HatchetAuthResponse; + +import retrofit.http.Field; +import retrofit.http.FormUrlEncoded; +import retrofit.http.GET; +import retrofit.http.Header; +import retrofit.http.POST; +import retrofit.http.Path; + +public interface HatchetAuth { + + @FormUrlEncoded + @POST("/authentication/password") + HatchetAuthResponse login( + @Field("username") String username, + @Field("password") String password, + @Field("grant_type") String grant_type + ); + + @FormUrlEncoded + @POST("/tokens/refresh/bearer") + HatchetAuthResponse getBearerAccessToken( + @Field("refresh_token") String refresh_token, + @Field("grant_type") String grant_type + ); + + @GET("/tokens/fetch/{tokentype}") + HatchetAuthResponse getAccessToken( + @Header("Authorization") String bearerAccessToken, + @Path("tokentype") String tokentype + ); + + @FormUrlEncoded + @POST("/registration/direct") + HatchetAuthResponse registerDirectly( + @Field("username") String username, + @Field("password") String password, + @Field("email") String email + ); + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuthenticatorUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuthenticatorUtils.java new file mode 100644 index 000000000..4fc49dbc9 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/HatchetAuthenticatorUtils.java @@ -0,0 +1,477 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication; + +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.authentication.models.HatchetAuthResponse; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.libtomahawk.utils.VariousUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdateListener; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.HashSet; +import java.util.List; + +import de.greenrobot.event.EventBus; +import retrofit.RestAdapter; +import retrofit.RetrofitError; +import retrofit.converter.GsonConverter; + +public class HatchetAuthenticatorUtils extends AuthenticatorUtils { + + private static final String TAG = HatchetAuthenticatorUtils.class.getSimpleName(); + + public static final String HATCHET_PRETTY_NAME = "Hatchet"; + + public static final String ACCOUNT_TYPE = "is.hatchet.account"; + + private static final String AUTH_TOKEN_HATCHET + = "is.hatchet.account.authtoken"; + + private static final String AUTH_TOKEN_EXPIRES_IN_HATCHET + = "is.hatchet.account.authtokenexpiresin"; + + private static final String MANDELLA_ACCESS_TOKEN_HATCHET + = "is.hatchet.account.mandellaaccesstoken"; + + private static final String MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET + = "is.hatchet.account.mandellaaccesstokenexpiresin"; + + private static final String CALUMET_ACCESS_TOKEN_HATCHET + = "is.hatchet.account.calumetaccesstoken"; + + private static final String CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET + = "is.hatchet.account.calumetaccesstokenexpiresin"; + + private static final String USER_ID_HATCHET + = "is.hatchet.account.userid"; + + private static final String HATCHET_AUTH_BASE_URL = "https://auth.hatchet.is/v1"; + + private static final String PARAMS_GRANT_TYPE_PASSWORD = "password"; + + private static final String PARAMS_GRANT_TYPE_REFRESHTOKEN = "refresh_token"; + + private static final String RESPONSE_TOKEN_TYPE_BEARER = "bearer"; + + private static final String RESPONSE_TOKEN_TYPE_CALUMET = "calumet"; + + private static final String RESPONSE_ERROR_INVALID_REQUEST = "invalid_request"; + + private static final int EXPIRING_LIMIT = 300; + + private final HatchetAuth mHatchetAuth; + + private final HashSet mCorrespondingRequestIds = new HashSet<>(); + + private ADeferredObject mGetUserIdPromise; + + boolean mWaitingForAccountRemoval; + + public static class UserLoginEvent { + + } + + public HatchetAuthenticatorUtils() { + super(TomahawkApp.PLUGINNAME_HATCHET, HATCHET_PRETTY_NAME); + + EventBus.getDefault().register(this); + + RestAdapter restAdapter = new RestAdapter.Builder() + .setLogLevel(RestAdapter.LogLevel.BASIC) + .setEndpoint(HATCHET_AUTH_BASE_URL) + .setConverter(new GsonConverter(GsonHelper.get())) + .build(); + mHatchetAuth = restAdapter.create(HatchetAuth.class); + } + + @SuppressWarnings("unused") + public void onEventAsync(InfoSystem.ResultsEvent event) { + if (event.mSuccess + && mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + if (event.mInfoRequestData.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS) { + List users = event.mInfoRequestData.getResultList(User.class); + if (users != null && users.get(0) != null) { + String userId = users.get(0).getId(); + storeUserId(userId); + mGetUserIdPromise.resolve(userId); + } + } + } + } + + public void onLogin(String username, String refreshToken, + long refreshTokenExpiresIn, String accessToken, long accessTokenExpiresIn) { + Log.d(TAG, + "Hatchet user '" + username + "' logged in successfully :)"); + if (username != null && !TextUtils.isEmpty(username) && refreshToken != null + && !TextUtils.isEmpty(refreshToken)) { + Log.d(TAG, "Hatchet auth token is served and yummy"); + Account account = new Account(username, ACCOUNT_TYPE); + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null) { + am.addAccountExplicitly(account, null, new Bundle()); + am.setAuthToken(account, AUTH_TOKEN_HATCHET, refreshToken); + am.setUserData(account, AUTH_TOKEN_EXPIRES_IN_HATCHET, + String.valueOf(refreshTokenExpiresIn)); + am.setUserData(account, MANDELLA_ACCESS_TOKEN_HATCHET, accessToken); + am.setUserData(account, MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, + String.valueOf(accessTokenExpiresIn)); + ensureAccessTokens(); + } + } + AuthenticatorManager.ConfigTestResultEvent event + = new AuthenticatorManager.ConfigTestResultEvent(); + event.mComponent = this; + event.mType = AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS; + EventBus.getDefault().post(event); + AuthenticatorManager.showToast(getPrettyName(), event); + } + + public void onLoginFailed(int type, String message) { + Log.d(TAG, + "Hatchet login failed :(, Type:" + type + ", Error: " + message); + AuthenticatorManager.ConfigTestResultEvent event + = new AuthenticatorManager.ConfigTestResultEvent(); + event.mComponent = this; + event.mType = type; + event.mMessage = message; + EventBus.getDefault().post(event); + AuthenticatorManager.showToast(getPrettyName(), event); + } + + public void onLogout() { + Log.d(TAG, "Hatchet user logged out"); + AuthenticatorManager.ConfigTestResultEvent event + = new AuthenticatorManager.ConfigTestResultEvent(); + event.mComponent = this; + event.mType = AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_LOGOUT; + EventBus.getDefault().post(event); + AuthenticatorManager.showToast(getPrettyName(), event); + } + + @Override + public String getDescription() { + return TomahawkApp.getContext().getString(R.string.preferences_hatchet_text, + HATCHET_PRETTY_NAME); + } + + @Override + public int getIconResourceId() { + return R.drawable.ic_hatchet; + } + + @Override + public int getUserIdEditTextHintResId() { + return R.string.login_username; + } + + @Override + public void register(final String name, final String password, final String email) { + ThreadManager.get().execute( + new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_AUTHENTICATING) { + @Override + public void run() { + try { + HatchetAuthResponse authResponse = + mHatchetAuth.registerDirectly(name, password, email); + if (authResponse != null) { + onLogin(name, + authResponse.refresh_token, + authResponse.refresh_token_expires_in, + authResponse.access_token, + authResponse.expires_in); + } else { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); + } + } catch (RetrofitError e) { + Log.d(TAG, + "register: " + e.getClass() + ": " + e.getLocalizedMessage()); + try { + HatchetAuthResponse authResponse = (HatchetAuthResponse) + e.getBodyAs(HatchetAuthResponse.class); + if (authResponse != null && authResponse.error != null && + authResponse.error.equals(RESPONSE_ERROR_INVALID_REQUEST)) { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + authResponse.error_description); + } else { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, + ""); + } + } catch (RuntimeException e1) { + onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + "Hatchet authentication error. Sorry, please try again later."); + } + } + } + } + ); + } + + @Override + public void login(final String name, final String password) { + ThreadManager.get().execute( + new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_AUTHENTICATING) { + @Override + public void run() { + try { + HatchetAuthResponse authResponse = mHatchetAuth + .login(name, password, PARAMS_GRANT_TYPE_PASSWORD); + if (authResponse != null) { + onLogin(authResponse.canonical_username, + authResponse.refresh_token, + authResponse.refresh_token_expires_in, + authResponse.access_token, + authResponse.expires_in); + } else { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, ""); + } + } catch (RetrofitError e) { + Log.d(TAG, "login: " + e.getClass() + ": " + e.getLocalizedMessage()); + try { + HatchetAuthResponse authResponse = (HatchetAuthResponse) + e.getBodyAs(HatchetAuthResponse.class); + if (authResponse != null && authResponse.error != null && + authResponse.error.equals(RESPONSE_ERROR_INVALID_REQUEST)) { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_INVALIDCREDS, + authResponse.error_description); + } else { + onLoginFailed( + AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_COMMERROR, + ""); + } + } catch (RuntimeException e1) { + onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + "Hatchet authentication error. Sorry, please try again later."); + } + } + } + } + ); + } + + @Override + public void logout() { + final AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null && getAccount() != null) { + am.removeAccount(getAccount(), null, null); + mWaitingForAccountRemoval = true; + am.addOnAccountsUpdatedListener(new OnAccountsUpdateListener() { + @Override + public void onAccountsUpdated(Account[] accounts) { + if (mWaitingForAccountRemoval && getAccount() == null) { + am.removeOnAccountsUpdatedListener(this); + mWaitingForAccountRemoval = false; + onLogout(); + } + } + }, null, false); + } + } + + public boolean isLoggedIn() { + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + return am != null && getAccount() != null + && am.peekAuthToken(getAccount(), AUTH_TOKEN_HATCHET) != null; + } + + public String getUserName() { + if (getAccount() != null) { + return getAccount().name; + } + return null; + } + + @Override + public boolean doesAllowRegistration() { + return true; + } + + public Promise getUserId() { + ADeferredObject getUserIdPromise = mGetUserIdPromise; + if (getUserIdPromise == null) { + getUserIdPromise = new ADeferredObject<>(); + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null && getAccount() != null) { + if (am.getUserData(getAccount(), USER_ID_HATCHET) != null) { + getUserIdPromise.resolve(am.getUserData(getAccount(), USER_ID_HATCHET)); + } else { + String requestId = InfoSystem.get().resolveUserId(getUserName()); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } else { + getUserIdPromise.reject(new Throwable("No account present.")); + mGetUserIdPromise = null; + } + } + return getUserIdPromise; + } + + /** + * Ensure that the calumet access token is available and valid. Get it from the cache if it + * hasn't yet expired. Otherwise refetch and cache it again. Also refetches the mandella access + * token if necessary. + * + * @return the calumet access token + */ + public String ensureAccessTokens() { + String refreshToken = null; + String calumetAccessToken = null; + String mandellaAccessToken = null; + int mandellaExpirationTime = -1; + int calumetExpirationTime = -1; + int currentTime = (int) (System.currentTimeMillis() / 1000); + + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null && getAccount() != null) { + mandellaAccessToken = am.getUserData(getAccount(), MANDELLA_ACCESS_TOKEN_HATCHET); + String mandellaExpirationTimeString = + am.getUserData(getAccount(), MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET); + if (mandellaExpirationTimeString != null) { + mandellaExpirationTime = Integer.valueOf(mandellaExpirationTimeString); + } + calumetAccessToken = am.getUserData(getAccount(), CALUMET_ACCESS_TOKEN_HATCHET); + String calumetExpirationTimeString = + am.getUserData(getAccount(), CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET); + if (calumetExpirationTimeString != null) { + calumetExpirationTime = Integer.valueOf(mandellaExpirationTimeString); + } + refreshToken = am.peekAuthToken(getAccount(), AUTH_TOKEN_HATCHET); + } + if (refreshToken != null && (mandellaAccessToken == null + || currentTime > mandellaExpirationTime - EXPIRING_LIMIT)) { + Log.d(TAG, "Mandella access token has expired, refreshing ..."); + mandellaAccessToken = fetchAccessToken(RESPONSE_TOKEN_TYPE_BEARER, refreshToken); + } + if (mandellaAccessToken != null && (calumetAccessToken == null + || currentTime > calumetExpirationTime - EXPIRING_LIMIT)) { + Log.d(TAG, "Calumet access token has expired, refreshing ..."); + calumetAccessToken = fetchAccessToken(RESPONSE_TOKEN_TYPE_CALUMET, mandellaAccessToken); + } + if (calumetAccessToken == null) { + Log.d(TAG, "Calumet access token couldn't be fetched. " + + "Most probably because no Hatchet account is logged in."); + } + return calumetAccessToken; + } + + /** + * Fetch the accessToken of the given tokenType by providing an existent token. The token is + * cached and then returned. + * + * @param tokenType The token type ("bearer"(aka mandella) or "calumet") + * @param token In the case of fetching the bearer token, this token should be the bearer + * refresh token provided by the initial auth process. If the calumet access + * token should be fetched, then the given token should be the bearer access + * token. + * @return the fetched access token + */ + public String fetchAccessToken(String tokenType, String token) { + String accessToken = null; + try { + HatchetAuthResponse authResponse; + if (tokenType.equals(RESPONSE_TOKEN_TYPE_BEARER)) { + authResponse = mHatchetAuth.getBearerAccessToken(token, + PARAMS_GRANT_TYPE_REFRESHTOKEN); + } else { + authResponse = mHatchetAuth.getAccessToken(RESPONSE_TOKEN_TYPE_BEARER + " " + token, + tokenType); + } + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null && getAccount() != null && authResponse.access_token != null) { + int currentTime = (int) (System.currentTimeMillis() / 1000); + long expirationTime = currentTime + authResponse.expires_in; + accessToken = authResponse.access_token; + if (VariousUtils.containsIgnoreCase(tokenType, RESPONSE_TOKEN_TYPE_BEARER)) { + am.setUserData(getAccount(), MANDELLA_ACCESS_TOKEN_HATCHET, accessToken); + am.setUserData(getAccount(), MANDELLA_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, + String.valueOf(expirationTime)); + } else { + am.setUserData(getAccount(), CALUMET_ACCESS_TOKEN_HATCHET, accessToken); + am.setUserData(getAccount(), CALUMET_ACCESS_TOKEN_EXPIRATIONTIME_HATCHET, + String.valueOf(expirationTime)); + } + Log.d(TAG, "Access token fetched, current time: '" + currentTime + + "', expiration time: '" + expirationTime + "'"); + } else { + onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + "Couldn't fetch access token"); + } + } catch (RetrofitError e) { + Log.e(TAG, "fetchAccessToken: " + e.getClass() + ": " + e.getLocalizedMessage()); + try { + HatchetAuthResponse authResponse = (HatchetAuthResponse) + e.getBodyAs(HatchetAuthResponse.class); + if (authResponse != null && (authResponse.error != null + || !VariousUtils.containsIgnoreCase(tokenType, authResponse.token_type))) { + logout(); + onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + "Please reenter your Hatchet credentials"); + } + } catch (RuntimeException e1) { + onLoginFailed(AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER, + "Hatchet authentication error. Sorry, please try again later."); + } + } + return accessToken; + } + + /** + * Get the Hatchet account from the AccountManager + * + * @return the account object or null if none could be found + */ + public static Account getAccount() { + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + if (am != null) { + Account[] accounts = am.getAccountsByType(ACCOUNT_TYPE); + if (accounts != null && accounts.length > 0) { + return accounts[0]; + } + } + return null; + } + + public static void storeUserId(String userId) { + AccountManager am = AccountManager.get(TomahawkApp.getContext()); + am.setUserData(getAccount(), USER_ID_HATCHET, userId); + EventBus.getDefault().post(new UserLoginEvent()); + } +} + diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/TomahawkAuthenticator.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/TomahawkAuthenticator.java new file mode 100644 index 000000000..1c6e6eac0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/TomahawkAuthenticator.java @@ -0,0 +1,126 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class TomahawkAuthenticator extends AbstractAccountAuthenticator { + + private static final String TAG = TomahawkAuthenticator.class.getSimpleName(); + + private final Context mContext; + + /** + * Service which handles authentication requests. + */ + public static class HatchetAuthenticationService extends Service { + + private TomahawkAuthenticator mTomahawkAuthenticator; + + @Override + public void onCreate() { + mTomahawkAuthenticator = new TomahawkAuthenticator(getApplicationContext()); + } + + @Override + public IBinder onBind(Intent intent) { + return mTomahawkAuthenticator.getIBinder(); + } + } + + public TomahawkAuthenticator(Context context) { + super(context); + + mContext = context; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws NetworkErrorException { + //final Intent intent = new Intent(mContext, TomahawkMainActivity.class); + //intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + //intent.putExtra(TomahawkService.AUTHENTICATOR_ID, TomahawkService.AUTHENTICATOR_ID_HATCHET); + //bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return new Bundle(); + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, + Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle options) throws NetworkErrorException { + final AccountManager am = AccountManager.get(mContext); + String authToken = am.peekAuthToken(account, authTokenType); + if (authToken != null && authToken.length() > 0) { + final Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, + HatchetAuthenticatorUtils.ACCOUNT_TYPE); + result.putString(AccountManager.KEY_AUTHTOKEN, authToken); + return result; + } + + /*final Intent intent = new Intent(mContext, TomahawkMainActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + intent.putExtra(TomahawkService.AUTHENTICATOR_ID, TomahawkService.AUTHENTICATOR_ID_HATCHET); + intent.putExtra(PARAMS_USERNAME, account.name); + intent.putExtra(PARAMS_TYPE, authTokenType);*/ + + //bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return new Bundle(); + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, + String[] features) throws NetworkErrorException { + final Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return result; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/authentication/models/HatchetAuthResponse.java b/app/src/main/java/org/tomahawk/libtomahawk/authentication/models/HatchetAuthResponse.java new file mode 100644 index 000000000..7fef04293 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/authentication/models/HatchetAuthResponse.java @@ -0,0 +1,58 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.authentication.models; + +public class HatchetAuthResponse { + + public String access_token; + + public String canonical_username; + + public String email; + + public String error; + + public String error_description; + + public String error_uri; + + public long expires_in; + + public String message; + + public String passwordresettoken; + + public String refresh_token; + + public long refresh_token_expires_in; + + public String socialaccesstoken; + + public long socialaccesstokenexpiration; + + public String socialaccesstokensecret; + + public String socialauthurl; + + public String token_type; + + public boolean verified; + + public HatchetAuthResponse() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Album.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Album.java new file mode 100644 index 000000000..bc4beeff4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Album.java @@ -0,0 +1,124 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Class which represents a Tomahawk {@link Album}. + */ +public class Album extends Cacheable implements AlphaComparable, ArtistAlphaComparable { + + public final static String RELEASETYPE_UNKNOWN = "unknown"; + + public final static String RELEASETYPE_ALBUM = "album"; + + public final static String RELEASETYPE_EPS = "ep"; + + private final String mName; + + private final Artist mArtist; + + private Image mImage; + + private String mReleaseType; + + /** + * Construct a new {@link Album} + */ + private Album(String albumName, Artist artist) { + super(Album.class, getCacheKey(albumName, artist.getName())); + + mName = albumName != null ? albumName : ""; + mArtist = artist; + } + + /** + * Returns the {@link Album} with the given album name and {@link org.tomahawk.libtomahawk.collection.Artist}. + * If none exists in our static {@link ConcurrentHashMap} yet, construct and add it. + */ + public static Album get(String albumName, Artist artist) { + Cacheable cacheable = get(Album.class, getCacheKey(albumName, artist.getName())); + return cacheable != null ? (Album) cacheable : new Album(albumName, artist); + } + + public static Album getByKey(String cacheKey) { + return (Album) get(Album.class, cacheKey); + } + + /** + * @return the {@link Album}'s name + */ + public String getName() { + return mName; + } + + /** + * @return the name that should be displayed + */ + public String getPrettyName() { + return getName().isEmpty() ? + TomahawkApp.getContext().getResources().getString(R.string.unknown_album) + : getName(); + } + + /** + * @return the filePath/url to this {@link Album}'s albumArt + */ + public Image getImage() { + return mImage; + } + + /** + * Set filePath/url to albumArt of this {@link Album} + * + * @param image filePath/url to albumArt of this {@link Album} + */ + public void setImage(Image image) { + mImage = image; + } + + /** + * @return the {@link Album}'s {@link Artist} + */ + public Artist getArtist() { + return mArtist; + } + + public String getReleaseType() { + return mReleaseType; + } + + public void setReleaseType(String releaseType) { + mReleaseType = releaseType; + } + + public String toShortString() { + return "'" + getName() + "'"; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( " + toShortString() + " by " + + getArtist().toShortString() + " )@" + Integer.toHexString(hashCode()); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparable.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparable.java new file mode 100644 index 000000000..e62413d3e --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparable.java @@ -0,0 +1,24 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +public interface AlphaComparable extends TomahawkComparable { + + String getName(); + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparator.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparator.java new file mode 100644 index 000000000..88cf114a7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/AlphaComparator.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import java.util.Comparator; + +/** + * This class is used to compare two {@link Object}s. + */ +public class AlphaComparator implements Comparator { + + /** + * The comparison method + * + * @param o1 First object to compare + * @param o2 Second object to compare + * @return int containing comparison score + */ + @Override + public int compare(AlphaComparable o1, AlphaComparable o2) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Artist.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Artist.java new file mode 100644 index 000000000..4e75dde15 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Artist.java @@ -0,0 +1,99 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +/** + * This class represents an {@link Artist}. + */ +public class Artist extends Cacheable implements AlphaComparable { + + public static final Artist COMPILATION_ARTIST = new Artist("Various Artists"); + + private final String mName; + + private ListItemString mBio; + + private Image mImage; + + /** + * Construct a new {@link Artist} + */ + private Artist(String artistName) { + super(Artist.class, getCacheKey(artistName)); + + mName = artistName != null ? artistName : ""; + } + + /** + * Returns the {@link Artist} with the given album name. If none exists in the cache yet, + * construct and add it. + */ + public static Artist get(String artistName) { + Cacheable cacheable = get(Artist.class, getCacheKey(artistName)); + return cacheable != null ? (Artist) cacheable : new Artist(artistName); + } + + public static Artist getByKey(String cacheKey) { + return (Artist) get(Artist.class, cacheKey); + } + + /** + * @return this object's name + */ + public String getName() { + return mName; + } + + /** + * @return the name that should be displayed + */ + public String getPrettyName() { + return getName().isEmpty() ? + TomahawkApp.getContext().getResources().getString(R.string.unknown_artist) + : getName(); + } + + public Image getImage() { + return mImage; + } + + public void setImage(Image image) { + mImage = image; + } + + public ListItemString getBio() { + return mBio; + } + + public void setBio(ListItemString bio) { + mBio = bio; + } + + public String toShortString() { + return "'" + getName() + "'"; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( " + toShortString() + " )@" + + Integer.toHexString(hashCode()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparable.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparable.java new file mode 100644 index 000000000..61f8171a4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparable.java @@ -0,0 +1,24 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +public interface ArtistAlphaComparable extends TomahawkComparable { + + Artist getArtist(); + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparator.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparator.java new file mode 100644 index 000000000..a85b2ce79 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/ArtistAlphaComparator.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import java.util.Comparator; + +/** + * This class is used to compare two {@link Object}s. + */ +public class ArtistAlphaComparator implements Comparator { + + /** + * The comparison method + * + * @param o1 First object to compare + * @param o2 Second object to compare + * @return int containing comparison score + */ + @Override + public int compare(ArtistAlphaComparable o1, ArtistAlphaComparable o2) { + return o1.getArtist().getName().compareToIgnoreCase(o2.getArtist().getName()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Cacheable.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Cacheable.java new file mode 100644 index 000000000..b3867a43f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Cacheable.java @@ -0,0 +1,82 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import android.util.Log; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This interface represents an item that can provide a corresponding cache key. + */ +public abstract class Cacheable { + + private static final String TAG = Cacheable.class.getSimpleName(); + + private static final Map> sCaches = new ConcurrentHashMap<>(); + + private String mCacheKey; + + protected Cacheable(Class clss, String cacheKey) { + mCacheKey = cacheKey; + + getCache(clss).put(cacheKey, this); + } + + protected static void put(Class clss, String cacheKey, Cacheable cacheable) { + getCache(clss).put(cacheKey, cacheable); + } + + public String getCacheKey() { + return mCacheKey; + } + + private static Map getCache(Class clss) { + Map cache = sCaches.get(clss); + if (cache == null) { + cache = new ConcurrentHashMap<>(); + sCaches.put(clss, cache); + } + return cache; + } + + protected static Cacheable get(Class clss, String cacheKey) { + return getCache(clss).get(cacheKey); + } + + protected static String getCacheKey(Object... objects) { + String result = ""; + for (int i = 0; i < objects.length; i++) { + Object o = objects[i]; + if (o != null) { + if (i > 0) { + result += "\t\t"; + } + if (o instanceof String) { + result += ((String) o); + } else if (o instanceof Boolean) { + result += ((Boolean) o) ? "1" : "0"; + } else { + Log.e(TAG, "getCacheKey - given Object type is not supported!"); + } + } + } + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Collection.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Collection.java new file mode 100644 index 000000000..eb81704ad --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Collection.java @@ -0,0 +1,90 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.Promise; + +import android.widget.ImageView; + +public abstract class Collection { + + public static final int SORT_NOT = -1; + + public static final int SORT_ALPHA = 0; + + public static final int SORT_ARTIST_ALPHA = 1; + + public static final int SORT_LAST_MODIFIED = 2; + + private final String mId; + + private final String mName; + + public Collection(String id, String name) { + mId = id; + mName = name; + } + + /** + * Load this {@link Collection}'s icon into the given {@link ImageView} + */ + public abstract void loadIcon(ImageView imageView, boolean grayOut); + + public String getId() { + return mId; + } + + public String getName() { + return mName; + } + + public Promise getQueries() { + return getQueries(SORT_NOT); + } + + public abstract Promise getQueries(int sortMode); + + public Promise, Throwable, Void> getArtists() { + return getArtists(SORT_NOT); + } + + public abstract Promise, Throwable, Void> getArtists(int sortMode); + + public Promise, Throwable, Void> getAlbumArtists() { + return getAlbumArtists(SORT_NOT); + } + + public abstract Promise, Throwable, Void> getAlbumArtists( + int sortMode); + + public Promise, Throwable, Void> getAlbums() { + return getAlbums(SORT_NOT); + } + + public abstract Promise, Throwable, Void> getAlbums(int sortMode); + + public abstract Promise, Throwable, Void> getArtistAlbums( + Artist artist); + + public abstract Promise getArtistTracks(Artist artist); + + public abstract Promise getAlbumTracks(Album album); + + public abstract Promise getAlbumTrackCount(Album album); + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionCursor.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionCursor.java new file mode 100644 index 000000000..81ae5684f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionCursor.java @@ -0,0 +1,185 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.Result; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionCursor { + + private final static String TAG = CollectionCursor.class.getSimpleName(); + + private SparseArray mCursorCache = new SparseArray<>(); + + private Cursor mCursor; + + private int mCursorCount; + + private List mItems; + + private Class mClass; + + private Resolver mResolver; + + private Playlist mPlaylist; + + public CollectionCursor(Cursor cursor, Class clss, Resolver resolver, Playlist playlist) { + mCursor = cursor; + mCursorCount = cursor.getCount(); + mClass = clss; + if (clss == PlaylistEntry.class || clss == Result.class) { + if (resolver != null) { + mResolver = resolver; + } else { + throw new RuntimeException("Resolver is required for " + + "CollectionCursor or CollectionCursor!"); + } + } + if (clss == PlaylistEntry.class) { + if (playlist != null) { + mPlaylist = playlist; + } else { + throw new RuntimeException("Playlist is required for " + + "CollectionCursor!"); + } + } + } + + public CollectionCursor(List items, Class clss) { + mItems = items; + mClass = clss; + } + + public CollectionCursor copy() { + CollectionCursor copy; + if (mCursor != null) { + copy = new CollectionCursor<>(mCursor, mClass, mResolver, mPlaylist); + SparseArray cacheCopy = mCursorCache.clone(); + copy.setCursorCache(cacheCopy); + } else { + List itemsCopy = new ArrayList<>(); + for (T item : mItems) { + itemsCopy.add(item); + } + copy = new CollectionCursor<>(itemsCopy, mClass); + } + return copy; + } + + public void close() { + if (mCursor != null) { + mCursor.close(); + } + } + + public void setCursorCache(SparseArray cursorCache) { + mCursorCache = cursorCache; + } + + public T get(int location) { + if (mCursor != null) { + if (mCursor.isClosed()) { + Log.d(TAG, "rawGet - Cursor has been closed."); + return null; + } + T cachedItem = mCursorCache.get(location); + if (cachedItem == null) { + mCursor.moveToPosition(location); + if (mClass == PlaylistEntry.class) { + Artist artist = Artist.get(mCursor.getString(0)); + Album album = Album.get(mCursor.getString(2), artist); + Track track = Track.get(mCursor.getString(3), album, artist); + track.setDuration(mCursor.getInt(4) * 1000); + track.setAlbumPos(mCursor.getInt(7)); + Result result = Result.get(mCursor.getString(5), track, mResolver); + Query query = Query.get(result, false); + query.addTrackResult(result, 1.0f); + PlaylistEntry entry = PlaylistEntry.get(mPlaylist.getId(), query, + IdGenerator.getLifetimeUniqueStringId()); + cachedItem = (T) entry; + } else if (mClass == Result.class) { + Artist artist = Artist.get(mCursor.getString(0)); + Album album = Album.get(mCursor.getString(2), artist); + Track track = Track.get(mCursor.getString(3), album, artist); + track.setDuration(mCursor.getInt(4) * 1000); + track.setAlbumPos(mCursor.getInt(7)); + Result result = Result.get(mCursor.getString(5), track, mResolver); + cachedItem = (T) result; + } else if (mClass == Album.class) { + Artist artist = Artist.get(mCursor.getString(1)); + Album album = Album.get(mCursor.getString(0), artist); + String imagePath = mCursor.getString(3); + if (!TextUtils.isEmpty(imagePath)) { + album.setImage(Image.get(imagePath, false)); + } + cachedItem = (T) album; + } else if (mClass == Artist.class) { + Artist artist = Artist.get(mCursor.getString(0)); + cachedItem = (T) artist; + } + mCursorCache.put(location, cachedItem); + } + return cachedItem; + } else { + return mItems.get(location); + } + } + + public int size() { + if (mCursor != null) { + return mCursorCount; + } else { + return mItems.size(); + } + } + + public String getArtistName(int location) { + if (mCursor != null) { + mCursor.moveToPosition(location); + if (mClass == PlaylistEntry.class || mClass == Result.class || mClass == Artist.class) { + return mCursor.getString(0); + } else if (mClass == Album.class) { + return mCursor.getString(1); + } + } else { + Object o = mItems.get(location); + if (o instanceof PlaylistEntry) { + return ((PlaylistEntry) o).getArtist().getName(); + } else if (o instanceof Result) { + return ((Result) o).getArtist().getName(); + } else if (o instanceof Album) { + return ((Album) o).getArtist().getName(); + } else if (o instanceof Artist) { + return ((Artist) o).getName(); + } + return ((ArtistAlphaComparable) mItems.get(location)).getArtist().getName(); + } + Log.e(TAG, "getArtistName(int location) - Couldn't return a string"); + return null; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionManager.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionManager.java new file mode 100644 index 000000000..6b2239826 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionManager.java @@ -0,0 +1,751 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.AlwaysCallback; +import org.jdeferred.Deferred; +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.jdeferred.android.AndroidDeferredManager; +import org.jdeferred.multiple.MultipleResults; +import org.jdeferred.multiple.OneReject; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.QueryParams; +import org.tomahawk.libtomahawk.infosystem.Relationship; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +public class CollectionManager { + + public static final String TAG = CollectionManager.class.getSimpleName(); + + private static class Holder { + + private static final CollectionManager instance = new CollectionManager(); + + } + + public static class AddedOrRemovedEvent { + + public Collection mCollection; + + } + + public static class UpdatedEvent { + + public Collection mCollection; + + public HashSet mUpdatedItemIds; + + } + + private final ConcurrentHashMap mCollections + = new ConcurrentHashMap<>(); + + private final HashSet mCorrespondingRequestIds = new HashSet<>(); + + private final HashSet mResolvingHatchetIds = new HashSet<>(); + + private final Set mShowAsDeletedPlaylistMap = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mShowAsCreatedPlaylistMap = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private AndroidDeferredManager mDeferredManager = new AndroidDeferredManager(); + + private CollectionManager() { + EventBus.getDefault().register(this); + + addCollection(new UserCollection()); + addCollection(new HatchetCollection()); + + fillUserWithStoredPlaylists(); + + fetchPlaylists(); + fetchLovedItemsPlaylist(); + fetchStarredAlbums(); + fetchStarredArtists(); + } + + public static CollectionManager get() { + return Holder.instance; + } + + @SuppressWarnings("unused") + public void onEventAsync(InfoSystem.OpLogIsEmptiedEvent event) { + for (Integer requestType : event.mRequestTypes) { + if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_SOCIALACTIONS) { + fetchStarredArtists(); + fetchStarredAlbums(); + fetchLovedItemsPlaylist(); + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS + || requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + fetchPlaylists(); + } + } + } + + @SuppressWarnings("unused") + public void onEventAsync(final InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + mCorrespondingRequestIds.remove(event.mInfoRequestData.getRequestId()); + handleHatchetPlaylistResponse(event.mInfoRequestData); + } + } + + @SuppressWarnings("unused") + public void onEventAsync(HatchetAuthenticatorUtils.UserLoginEvent event) { + fetchPlaylists(); + fetchLovedItemsPlaylist(); + fetchStarredAlbums(); + fetchStarredArtists(); + } + + @SuppressWarnings("unused") + public void onEventAsync(final DatabaseHelper.PlaylistsUpdatedEvent event) { + fillUserWithStoredPlaylists(); + } + + /** + * Fill the logged-in User object with the playlists we have stored in the database. + */ + public Promise fillUserWithStoredPlaylists() { + final ADeferredObject deferred = new ADeferredObject<>(); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(final User user) { + TomahawkRunnable r = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_DATABASEACTION) { + @Override + public void run() { + user.setPlaylists(DatabaseHelper.get().getPlaylists()); + Playlist favorites = DatabaseHelper.get().getLovedItemsPlaylist(); + if (favorites != null) { + user.setFavorites(favorites); + } + deferred.resolve(null); + } + }; + ThreadManager.get().execute(r); + } + }); + return deferred; + } + + public void addCollection(Collection collection) { + mCollections.put(collection.getId(), collection); + AddedOrRemovedEvent event = new AddedOrRemovedEvent(); + event.mCollection = collection; + EventBus.getDefault().post(event); + } + + public void removeCollection(Collection collection) { + mCollections.remove(collection.getId()); + AddedOrRemovedEvent event = new AddedOrRemovedEvent(); + event.mCollection = collection; + EventBus.getDefault().post(event); + } + + public Collection getCollection(String collectionId) { + return mCollections.get(collectionId); + } + + /** + * Convenience method + */ + public UserCollection getUserCollection() { + return (UserCollection) mCollections.get(TomahawkApp.PLUGINNAME_USERCOLLECTION); + } + + /** + * Convenience method + */ + public HatchetCollection getHatchetCollection() { + return (HatchetCollection) mCollections.get(TomahawkApp.PLUGINNAME_HATCHET); + } + + public java.util.Collection getCollections() { + return mCollections.values(); + } + + /** + * Remove or add a lovedItem-query from the LovedItems-Playlist, depending on whether or not it + * is already a lovedItem + */ + public void setLovedItem(final Query query, boolean loved) { + boolean isLoved = DatabaseHelper.get().isItemLoved(query); + if (loved != isLoved) { + toggleLovedItem(query); + } else { + Log.e(TAG, "Track " + query.getName() + " by " + query.getArtist().getName() + " on " + + query.getAlbum().getName() + " was already loved!"); + } + } + + /** + * Remove or add a lovedItem-query from the LovedItems-Playlist, depending on whether or not it + * is already a lovedItem + */ + public void toggleLovedItem(final Query query) { + boolean doSweetSweetLovin = !DatabaseHelper.get().isItemLoved(query); + Log.d(TAG, "Hatchet sync - " + (doSweetSweetLovin ? "loved" : "unloved") + " track " + + query.getName() + " by " + query.getArtist().getName() + " on " + + query.getAlbum().getName()); + DatabaseHelper.get().setLovedItem(query, doSweetSweetLovin); + UpdatedEvent event = new UpdatedEvent(); + event.mUpdatedItemIds = new HashSet<>(); + event.mUpdatedItemIds.add(query.getCacheKey()); + EventBus.getDefault().post(event); + final AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + if (doSweetSweetLovin) { + InfoSystem.get().sendRelationshipPostStruct(hatchetAuthUtils, query); + } else { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + Relationship relationship = result.getRelationship(query); + if (relationship == null) { + Log.e(TAG, "Can't unlove track, because there's no relationshipId" + + " associated with it."); + return; + } + InfoSystem.get().deleteRelationship( + hatchetAuthUtils, relationship.getCacheKey()); + } + }); + } + } + + public void toggleLovedItem(final Artist artist) { + boolean doSweetSweetLovin = !getUserCollection().isLoved(artist); + Log.d(TAG, "Hatchet sync - " + (doSweetSweetLovin ? "starred" : "unstarred") + " artist " + + artist.getName()); + final AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + if (doSweetSweetLovin) { + List artists = new ArrayList<>(); + artists.add(artist); + List lastModifieds = new ArrayList<>(); + lastModifieds.add(System.currentTimeMillis()); + getUserCollection().addLovedArtists(artists, lastModifieds); + InfoSystem.get().sendRelationshipPostStruct(hatchetAuthUtils, artist); + } else { + getUserCollection().removeLoved(artist); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + Relationship relationship = result.getRelationship(artist); + if (relationship == null) { + Log.e(TAG, "Can't unlove artist, because there's no relationship" + + " associated with it."); + return; + } + InfoSystem.get().deleteRelationship( + hatchetAuthUtils, relationship.getCacheKey()); + } + }); + } + UpdatedEvent event = new UpdatedEvent(); + event.mUpdatedItemIds = new HashSet<>(); + event.mUpdatedItemIds.add(artist.getCacheKey()); + EventBus.getDefault().post(event); + } + + public void toggleLovedItem(final Album album) { + boolean doSweetSweetLovin = !getUserCollection().isLoved(album); + Log.d(TAG, "Hatchet sync - " + (doSweetSweetLovin ? "starred" : "unstarred") + " album " + + album.getName() + " by " + album.getArtist().getName()); + final AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + if (doSweetSweetLovin) { + List albums = new ArrayList<>(); + albums.add(album); + List lastModifieds = new ArrayList<>(); + lastModifieds.add(System.currentTimeMillis()); + getUserCollection().addLovedAlbums(albums, lastModifieds); + InfoSystem.get().sendRelationshipPostStruct(hatchetAuthUtils, album); + } else { + getUserCollection().removeLoved(album); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + Relationship relationship = result.getRelationship(album); + if (relationship == null) { + Log.e(TAG, "Can't unlove album, because there's no relationship" + + " associated with it."); + return; + } + InfoSystem.get().deleteRelationship( + hatchetAuthUtils, relationship.getCacheKey()); + } + }); + } + UpdatedEvent event = new UpdatedEvent(); + event.mUpdatedItemIds = new HashSet<>(); + event.mUpdatedItemIds.add(album.getCacheKey()); + EventBus.getDefault().post(event); + } + + /** + * Fetch the lovedItems Playlist from the Hatchet API and store it in the local db, if the log + * of pending operations is empty. Meaning if every love/unlove has already been delivered to + * the API. + */ + public void fetchLovedItemsPlaylist() { + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + Log.d(TAG, "Hatchet sync - fetching loved tracks"); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolveLovedItems(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } else { + Log.d(TAG, "Hatchet sync - sending logged ops before fetching loved tracks"); + HatchetAuthenticatorUtils hatchetAuthUtils = (HatchetAuthenticatorUtils) + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + + /** + * Fetch the starred artists from the Hatchet API and store it in the local db, if the log of + * pending operations is empty. Meaning if every love/unlove has already been delivered to the + * API. + */ + public void fetchStarredArtists() { + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + Log.d(TAG, "Hatchet sync - fetching starred artists"); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolveLovedArtists(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } else { + Log.d(TAG, "Hatchet sync - sending logged ops before fetching starred artists"); + HatchetAuthenticatorUtils hatchetAuthUtils = (HatchetAuthenticatorUtils) + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + + /** + * Fetch the starred albums from the Hatchet API and store it in the local db, if the log of + * pending operations is empty. Meaning if every love/unlove has already been delivered to the + * API. + */ + public void fetchStarredAlbums() { + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + Log.d(TAG, "Hatchet sync - fetching starred albums"); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolveLovedAlbums(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } else { + Log.d(TAG, "Hatchet sync - sending logged ops before fetching starred albums"); + HatchetAuthenticatorUtils hatchetAuthUtils = (HatchetAuthenticatorUtils) + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + + /** + * Fetch the Playlists from the Hatchet API and store it in the local db. + */ + public void fetchPlaylists() { + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + Log.d(TAG, "Hatchet sync - fetching playlists"); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolvePlaylists(user, true); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } else { + Log.d(TAG, "Hatchet sync - sending logged ops before fetching playlists"); + HatchetAuthenticatorUtils hatchetAuthUtils = (HatchetAuthenticatorUtils) + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + + /** + * Fetch the Playlist entries from the Hatchet API and store them in the local db. + */ + public void fetchHatchetPlaylistEntries(String playlistId) { + String hatchetId = DatabaseHelper.get().getPlaylistHatchetId(playlistId); + String name = DatabaseHelper.get().getPlaylistName(playlistId); + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + if (hatchetId != null) { + if (!mResolvingHatchetIds.contains(hatchetId)) { + Log.d(TAG, "Hatchet sync - fetching entry list for playlist \"" + name + + "\", hatchetId: " + hatchetId); + mResolvingHatchetIds.add(hatchetId); + QueryParams params = new QueryParams(); + params.playlist_local_id = playlistId; + params.playlist_id = hatchetId; + String requestid = InfoSystem.get().resolve( + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES, params, + true); + mCorrespondingRequestIds.add(requestid); + } else { + Log.d(TAG, "Hatchet sync - couldn't fetch entry list for playlist \"" + + name + "\", because this playlist is already waiting for its entry " + + "list, hatchetId: " + hatchetId); + } + } else { + Log.d(TAG, "Hatchet sync - couldn't fetch entry list for playlist \"" + + name + "\" because hatchetId was null"); + } + } else { + Log.d(TAG, "Hatchet sync - sending logged ops before fetching entry list for playlist" + + " \"" + name + "\", hatchetId: " + hatchetId); + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + + public void handleHatchetPlaylistResponse(InfoRequestData data) { + if (data.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_PLAYLISTS) { + List storedLists = DatabaseHelper.get().getPlaylists(); + HashMap storedListsMap = new HashMap<>(); + for (Playlist storedList : storedLists) { + if (storedListsMap.containsKey(storedList.getHatchetId())) { + Log.e(TAG, "Hatchet sync - playlist \"" + storedList.getName() + + "\" is duplicated ... deleting"); + if (TextUtils.isEmpty(storedList.getCurrentRevision())) { + DatabaseHelper.get().deletePlaylist(storedList.getId()); + } else { + Playlist otherStoredList = storedListsMap.get(storedList.getHatchetId()); + DatabaseHelper.get().deletePlaylist(otherStoredList.getId()); + storedListsMap.put(storedList.getHatchetId(), storedList); + } + } else { + storedListsMap.put(storedList.getHatchetId(), storedList); + } + } + List results = data.getResultList(User.class); + if (results == null || results.size() == 0) { + Log.e(TAG, "Hatchet sync - something went wrong. Got no user object back :("); + return; + } + User user = results.get(0); + List fetchedLists = user.getPlaylists(); + Log.d(TAG, "Hatchet sync - playlist count in database: " + storedLists.size() + + ", playlist count on Hatchet: " + fetchedLists.size()); + for (final Playlist fetchedList : fetchedLists) { + Playlist storedList = storedListsMap.remove(fetchedList.getHatchetId()); + if (storedList == null) { + if (mShowAsDeletedPlaylistMap.contains(fetchedList.getHatchetId())) { + Log.d(TAG, "Hatchet sync - " + fetchedList + + " didn't exist in database, but was marked as showAsDeleted so" + + " we don't store it."); + } else { + if (mShowAsCreatedPlaylistMap.contains(fetchedList.getHatchetId())) { + mShowAsCreatedPlaylistMap.remove(fetchedList.getHatchetId()); + Log.d(TAG, "Hatchet sync - " + fetchedList + + " is no longer marked as showAsCreated, since it seems to" + + " have arrived on the server"); + } + Log.d(TAG, "Hatchet sync - " + fetchedList + " didn't exist in database" + + " ... storing and fetching entries"); + DatabaseHelper.get().storePlaylist(fetchedList, false); + fetchHatchetPlaylistEntries(fetchedList.getId()); + } + } else if (!storedList.getCurrentRevision() + .equals(fetchedList.getCurrentRevision())) { + Log.d(TAG, "Hatchet sync - revision differed for " + fetchedList + + " ... fetching entries"); + fetchHatchetPlaylistEntries(storedList.getId()); + } else if (!storedList.getName().equals(fetchedList.getName())) { + Log.d(TAG, "Hatchet sync - title differed for stored " + storedList + + " and fetched " + fetchedList + " ... renaming"); + DatabaseHelper.get().renamePlaylist(storedList, fetchedList.getName()); + } + } + for (Playlist storedList : storedListsMap.values()) { + if (storedList.getHatchetId() == null + || !mShowAsCreatedPlaylistMap.contains(storedList.getHatchetId())) { + Log.d(TAG, "Hatchet sync - " + storedList + + " doesn't exist on Hatchet ... deleting"); + DatabaseHelper.get().deletePlaylist(storedList.getId()); + } else { + Log.d(TAG, "Hatchet sync - " + storedList + + " doesn't exist on Hatchet, but we don't delete it since it's" + + " marked as showAsCreated"); + } + } + } else if (data.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + if (data.getHttpType() == InfoRequestData.HTTPTYPE_GET) { + List results = data.getResultList(Playlist.class); + if (results != null && results.size() > 0) { + Playlist filledList = results.get(0); + if (filledList != null) { + Log.d(TAG, "Hatchet sync - received entry list for " + filledList); + DatabaseHelper.get().storePlaylist(filledList, false); + mResolvingHatchetIds.remove(filledList.getHatchetId()); + } + } + } else if (data.getHttpType() == InfoRequestData.HTTPTYPE_POST) { + String hatchetId = DatabaseHelper.get() + .getPlaylistHatchetId(data.getQueryParams().playlist_local_id); + if (hatchetId != null) { + mShowAsCreatedPlaylistMap.add(hatchetId); + Log.d(TAG, "Hatchet sync - created playlist and marked as showAsCreated, id: " + + data.getQueryParams().playlist_local_id + ", hatchetId: " + + hatchetId); + } + } + } else if (data.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDITEMS) { + List results = data.getResultList(User.class); + if (results == null || results.size() == 0) { + Log.e(TAG, "Hatchet sync - something went wrong. Got no user object back :("); + return; + } + User user = results.get(0); + Playlist fetchedList = user.getFavorites(); + if (fetchedList != null) { + HatchetAuthenticatorUtils hatchetAuthUtils = + (HatchetAuthenticatorUtils) AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + String userName = hatchetAuthUtils.getUserName(); + fetchedList.setName(TomahawkApp.getContext() + .getString(R.string.users_favorites_suffix, userName)); + Log.d(TAG, "Hatchet sync - received list of loved tracks, count: " + + fetchedList.size()); + DatabaseHelper.get().storeLovedItemsPlaylist(fetchedList, true); + } + } else if (data.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDALBUMS) { + List results = data.getResultList(User.class); + if (results == null || results.size() == 0) { + Log.e(TAG, "Hatchet sync - something went wrong. Got no user object back :("); + return; + } + User user = results.get(0); + List fetchedAlbums = user.getStarredAlbums(); + Log.d(TAG, "Hatchet sync - received list of starred albums, count: " + + fetchedAlbums.size()); + List lastModifieds = new ArrayList<>(); + for (Album album : fetchedAlbums) { + Relationship relationship = user.getRelationship(album); + if (relationship == null) { + Log.e(TAG, "Hatchet sync - couldn't find associated relationship for " + album); + lastModifieds.add(Long.MAX_VALUE); + } else { + lastModifieds.add(relationship.getDate().getTime()); + } + } + getUserCollection().addLovedAlbums(fetchedAlbums, lastModifieds); + } else if (data.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDARTISTS) { + List results = data.getResultList(User.class); + if (results == null || results.size() == 0) { + Log.e(TAG, "Hatchet sync - something went wrong. Got no user object back :("); + return; + } + User user = results.get(0); + List fetchedArtists = user.getStarredArtists(); + Log.d(TAG, "Hatchet sync - received list of starred artists, count: " + + fetchedArtists.size()); + List lastModifieds = new ArrayList<>(); + for (Artist artist : fetchedArtists) { + Relationship relationship = user.getRelationship(artist); + if (relationship == null) { + Log.e(TAG, + "Hatchet sync - couldn't find associated relationship for " + artist); + lastModifieds.add(Long.MAX_VALUE); + } else { + lastModifieds.add(relationship.getDate().getTime()); + } + } + getUserCollection().addLovedArtists(fetchedArtists, lastModifieds); + } + } + + public void deletePlaylist(String playlistId) { + String playlistName = DatabaseHelper.get().getPlaylistName(playlistId); + if (playlistName != null) { + Log.d(TAG, "Hatchet sync - deleting playlist \"" + playlistName + "\", id: " + + playlistId); + Playlist playlist = DatabaseHelper.get().getEmptyPlaylist(playlistId); + if (playlist.getHatchetId() != null) { + mShowAsDeletedPlaylistMap.add(playlist.getHatchetId()); + } + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().deletePlaylist(hatchetAuthUtils, playlistId); + DatabaseHelper.get().deletePlaylist(playlistId); + } else { + Log.e(TAG, "Hatchet sync - couldn't delete playlist with id: " + playlistId); + } + } + + public void createPlaylist(Playlist playlist) { + Log.d(TAG, "Hatchet sync - creating " + playlist); + DatabaseHelper.get().storePlaylist(playlist, false); + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendPlaylistPostStruct( + hatchetAuthUtils, playlist.getId(), playlist.getName()); + InfoSystem.get().sendPlaylistEntriesPostStruct(hatchetAuthUtils, + playlist.getId(), playlist.getEntries()); + } + + public void addPlaylistEntries(String playlistId, ArrayList entries) { + String playlistName = DatabaseHelper.get().getPlaylistName(playlistId); + if (playlistName != null) { + Log.d(TAG, "Hatchet sync - adding " + entries.size() + " entries to \"" + + playlistName + "\", id: " + playlistId); + DatabaseHelper.get().addEntriesToPlaylist(playlistId, entries); + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendPlaylistEntriesPostStruct(hatchetAuthUtils, playlistId, entries); + } else { + Log.e(TAG, "Hatchet sync - couldn't add " + entries.size() + + " entries to playlist with id: " + playlistId); + } + } + + public void deletePlaylistEntry(String localPlaylistId, String entryId) { + String playlistName = DatabaseHelper.get().getPlaylistName(localPlaylistId); + if (playlistName != null) { + Log.d(TAG, "Hatchet sync - deleting playlist entry in \"" + playlistName + + "\", localPlaylistId: " + localPlaylistId + ", entryId: " + entryId); + DatabaseHelper.get().deleteEntryInPlaylist(localPlaylistId, entryId); + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().deletePlaylistEntry(hatchetAuthUtils, localPlaylistId, entryId); + } else { + Log.e(TAG, "Hatchet sync - couldn't delete entry in playlist, localPlaylistId: " + + localPlaylistId + ", entryId: " + entryId); + } + } + + public Promise, Throwable, Void> getAvailableCollections(Artist artist) { + final List collections = new ArrayList<>(); + List promises = new ArrayList<>(); + for (final Collection collection : mCollections.values()) { + if (!collection.getId().equals(TomahawkApp.PLUGINNAME_HATCHET)) { + promises.add(collection.getArtistAlbums(artist)); + collections.add(collection); + } + } + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + mDeferredManager.when(promises.toArray(new Promise[promises.size()])).always( + new AlwaysCallback() { + @Override + public void onAlways(Promise.State state, MultipleResults resolved, + OneReject rejected) { + List availableCollections = new ArrayList<>(); + for (int i = 0; i < resolved.size(); i++) { + CollectionCursor cursor + = (CollectionCursor) resolved.get(i).getResult(); + if (cursor != null) { + if (cursor.size() > 0) { + availableCollections.add(collections.get(i)); + } + cursor.close(); + } + } + availableCollections.add(mCollections.get(TomahawkApp.PLUGINNAME_HATCHET)); + deferred.resolve(availableCollections); + } + }); + return deferred; + } + + public Promise, Throwable, Void> getAvailableCollections(Album album) { + final List collections = new ArrayList<>(); + List promises = new ArrayList<>(); + for (final Collection collection : mCollections.values()) { + if (!collection.getId().equals(TomahawkApp.PLUGINNAME_HATCHET)) { + promises.add(collection.getAlbumTracks(album)); + collections.add(collection); + } + } + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + mDeferredManager.when(promises.toArray(new Promise[promises.size()])).always( + new AlwaysCallback() { + @Override + public void onAlways(Promise.State state, MultipleResults resolved, + OneReject rejected) { + List availableCollections = new ArrayList<>(); + for (int i = 0; i < resolved.size(); i++) { + Playlist playlist = (Playlist) resolved.get(i).getResult(); + if (playlist != null) { + if (playlist.size() > 0) { + availableCollections.add(collections.get(i)); + } + } + } + availableCollections.add(mCollections.get(TomahawkApp.PLUGINNAME_HATCHET)); + deferred.resolve(availableCollections); + } + }); + return deferred; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionUtils.java new file mode 100644 index 000000000..bef2358b9 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/CollectionUtils.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.libtomahawk.resolver.Query; + +public class CollectionUtils { + + public static boolean allFromOneArtist(CollectionCursor items) { + if (items == null || items.size() < 2) { + return true; + } + Query item = items.get(0); + for (int i = 1; i < items.size(); i++) { + Query itemToCompare = items.get(i); + if (itemToCompare.getArtist() != item.getArtist()) { + return false; + } + item = itemToCompare; + } + return true; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/DbCollection.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/DbCollection.java new file mode 100644 index 000000000..0165fa5d8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/DbCollection.java @@ -0,0 +1,437 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.Deferred; +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.database.CollectionDb; +import org.tomahawk.libtomahawk.database.CollectionDbManager; +import org.tomahawk.libtomahawk.resolver.FuzzyIndex; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.Result; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.database.Cursor; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * This class represents a Collection which contains tracks/albums/artists which are being stored in + * a local sqlite db. + */ +public abstract class DbCollection extends Collection { + + private final static String TAG = DbCollection.class.getSimpleName(); + + private Set mWaitingQueries = Collections + .newSetFromMap(new ConcurrentHashMap()); + + private Resolver mResolver; + + private boolean mInitialized; + + public class InitializedEvent { + + String collectionId; + } + + public DbCollection(Resolver resolver) { + super(resolver.getId(), resolver.getPrettyName()); + + mResolver = resolver; + } + + public boolean isInitialized() { + return mInitialized; + } + + public void setInitialized(boolean initialized) { + mInitialized = initialized; + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + TomahawkRunnable r = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_DATABASEACTION) { + @Override + public void run() { + invokeWaitingJobs(); + } + }; + ThreadManager.get().execute(r); + InitializedEvent event = new InitializedEvent(); + event.collectionId = collectionId; + EventBus.getDefault().post(event); + } + }); + } + + public String getIconBackgroundPath() { + if (mResolver instanceof ScriptResolver) { + ScriptAccount account = ((ScriptResolver) mResolver).getScriptAccount(); + return account.getIconBackgroundPath(); + } + return null; + } + + public abstract Promise getCollectionId(); + + public void resolve(final Query query) { + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + final CollectionDb db = CollectionDbManager.get().getCollectionDb(collectionId); + if (!mInitialized) { + mWaitingQueries.add(query); + Log.d(TAG, collectionId + " - Added query to the waiting queue because the " + + "FuzzyIndex is still initializing."); + } else { + TomahawkRunnable r = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_RESOLVING) { + @Override + public void run() { + List indexResults = + db.getFuzzyIndex().searchIndex(query); + if (indexResults.size() > 0) { + String[] ids = new String[indexResults.size()]; + for (int i = 0; i < indexResults.size(); i++) { + FuzzyIndex.IndexResult indexResult = indexResults.get(i); + ids[i] = String.valueOf(indexResult.id); + } + CollectionDb.WhereInfo whereInfo = new CollectionDb.WhereInfo(); + whereInfo.connection = "OR"; + whereInfo.where.put(CollectionDb.ID, ids); + Cursor cursor = db.tracks(whereInfo, null); + CollectionCursor collectionCursor = new CollectionCursor<>( + cursor, Result.class, mResolver, null); + ArrayList results = new ArrayList<>(); + for (int i = 0; i < collectionCursor.size(); i++) { + results.add(collectionCursor.get(i)); + } + collectionCursor.close(); + PipeLine.get().reportResults(query, results, mResolver.getId()); + } + } + }; + ThreadManager.get().execute(r, query); + } + } + }); + } + + private synchronized void invokeWaitingJobs() { + Log.d(TAG, "Resolving " + mWaitingQueries.size() + " waiting queries"); + for (Query query : mWaitingQueries) { + resolve(query); + } + mWaitingQueries.clear(); + } + + @Override + public Promise getQueries(final int sortMode) { + final Deferred deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + String[] orderBy; + switch (sortMode) { + case SORT_ALPHA: + orderBy = new String[]{CollectionDb.TRACKS_TRACK}; + break; + case SORT_ARTIST_ALPHA: + orderBy = new String[]{CollectionDb.ARTISTS_ARTIST}; + break; + case SORT_LAST_MODIFIED: + orderBy = new String[]{CollectionDb.TRACKS_LASTMODIFIED + " DESC"}; + break; + default: + Log.e(TAG, + collectionId + " - getQueries - sortMode not supported!"); + return; + } + CollectionDb db = CollectionDbManager.get().getCollectionDb(collectionId); + String currentRevision = String.valueOf(db.tracksCurrentRevision()); + Playlist playlist = Playlist.get( + collectionId + "_tracks_" + currentRevision + "_" + sortMode); + if (playlist.getCurrentRevision().isEmpty()) { + Cursor cursor = db.tracks(null, orderBy); + if (cursor == null) { + deferred.resolve(null); + return; + } + CollectionCursor collectionCursor + = new CollectionCursor<>( + cursor, PlaylistEntry.class, mResolver, playlist); + playlist.setCursor(collectionCursor); + playlist.setFilled(true); + playlist.setCurrentRevision(currentRevision); + } + deferred.resolve(playlist); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise, Throwable, Void> getArtists(final int sortMode) { + final Deferred, Throwable, Void> deferred + = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + String[] orderBy; + switch (sortMode) { + case SORT_ALPHA: + orderBy = new String[]{ + CollectionDb.ARTISTS_ARTIST + " COLLATE NOCASE "}; + break; + case SORT_LAST_MODIFIED: + orderBy = new String[]{CollectionDb.ARTISTS_LASTMODIFIED + " DESC"}; + break; + default: + Log.e(TAG, + collectionId + " - getArtists - sortMode not supported!"); + return; + } + Cursor cursor = CollectionDbManager.get().getCollectionDb(collectionId) + .artists(orderBy); + CollectionCursor collectionCursor = + new CollectionCursor<>(cursor, Artist.class, null, null); + deferred.resolve(collectionCursor); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise, Throwable, Void> getAlbumArtists(final int sortMode) { + final Deferred, Throwable, Void> deferred + = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + String[] orderBy; + switch (sortMode) { + case SORT_ALPHA: + orderBy = new String[]{ + CollectionDb.ARTISTS_ARTIST + " COLLATE NOCASE "}; + break; + case SORT_LAST_MODIFIED: + orderBy = new String[]{CollectionDb.ARTISTS_LASTMODIFIED + " DESC"}; + break; + default: + Log.e(TAG, collectionId + + " - getAlbumArtists - sortMode not supported!"); + return; + } + Cursor cursor = CollectionDbManager.get().getCollectionDb(collectionId) + .albumArtists(orderBy); + CollectionCursor collectionCursor = + new CollectionCursor<>(cursor, Artist.class, null, null); + deferred.resolve(collectionCursor); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise, Throwable, Void> getAlbums(final int sortMode) { + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + String[] orderBy; + switch (sortMode) { + case SORT_ALPHA: + orderBy = new String[]{ + CollectionDb.ALBUMS_ALBUM + " COLLATE NOCASE "}; + break; + case SORT_ARTIST_ALPHA: + orderBy = new String[]{ + CollectionDb.ARTISTS_ARTIST + " COLLATE NOCASE "}; + break; + case SORT_LAST_MODIFIED: + orderBy = new String[]{CollectionDb.ALBUMS_LASTMODIFIED + " DESC"}; + break; + default: + Log.e(TAG, collectionId + " - getAlbums - sortMode not supported!"); + return; + } + Cursor cursor = CollectionDbManager.get().getCollectionDb(collectionId) + .albums(orderBy); + CollectionCursor collectionCursor = + new CollectionCursor<>(cursor, Album.class, null, null); + deferred.resolve(collectionCursor); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise, Throwable, Void> getArtistAlbums(final Artist artist) { + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String result) { + new Thread(new Runnable() { + @Override + public void run() { + Cursor cursor = CollectionDbManager.get().getCollectionDb(result) + .artistAlbums(artist.getName(), ""); + if (cursor == null) { + deferred.resolve(null); + return; + } + CollectionCursor collectionCursor = + new CollectionCursor<>(cursor, Album.class, null, null); + deferred.resolve(collectionCursor); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise getArtistTracks(final Artist artist) { + final Deferred deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + CollectionDb db = CollectionDbManager.get().getCollectionDb(collectionId); + String currentRevision = + String.valueOf(db.artistCurrentRevision(artist.getName(), "")); + Playlist playlist = Playlist.get( + collectionId + "_" + artist.getCacheKey() + "_" + currentRevision); + if (playlist.getCurrentRevision().isEmpty()) { + Cursor cursor = db.artistTracks(artist.getName(), ""); + if (cursor == null) { + deferred.resolve(null); + return; + } + CollectionCursor collectionCursor + = new CollectionCursor<>( + cursor, PlaylistEntry.class, mResolver, playlist); + playlist.setCursor(collectionCursor); + playlist.setFilled(true); + playlist.setCurrentRevision(currentRevision); + } + deferred.resolve(playlist); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise getAlbumTracks(final Album album) { + final Deferred deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + CollectionDb db = CollectionDbManager.get().getCollectionDb(collectionId); + String currentRevision = String.valueOf(db.albumCurrentRevision( + album.getName(), album.getArtist().getName(), "")); + Playlist playlist = Playlist.get( + collectionId + "_" + album.getCacheKey() + "_" + currentRevision); + if (playlist.getCurrentRevision().isEmpty()) { + Cursor cursor = db.albumTracks( + album.getName(), album.getArtist().getName(), ""); + if (cursor == null) { + deferred.resolve(null); + return; + } + CollectionCursor collectionCursor + = new CollectionCursor<>( + cursor, PlaylistEntry.class, mResolver, playlist); + playlist.setCursor(collectionCursor); + playlist.setFilled(true); + playlist.setCurrentRevision(currentRevision); + } + deferred.resolve(playlist); + } + }).start(); + } + }); + return deferred; + } + + @Override + public Promise getAlbumTrackCount(final Album album) { + final Deferred deferred = new ADeferredObject<>(); + getCollectionId().done(new DoneCallback() { + @Override + public void onDone(final String collectionId) { + new Thread(new Runnable() { + @Override + public void run() { + Cursor cursor = CollectionDbManager.get().getCollectionDb(collectionId) + .albumTracks(album.getName(), album.getArtist().getName(), ""); + if (cursor == null) { + deferred.resolve(null); + return; + } + deferred.resolve(cursor.getCount()); + cursor.close(); + } + }).start(); + } + }); + return deferred; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/HatchetCollection.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/HatchetCollection.java new file mode 100644 index 000000000..74e6f47aa --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/HatchetCollection.java @@ -0,0 +1,212 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.Deferred; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class holds the metadata retrieved via Hatchet. + */ +public class HatchetCollection extends Collection { + + private final Set mAlbums + = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mArtists + = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mAlbumArtists + = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final ConcurrentHashMap mAlbumTracks + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap> mArtistAlbums + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap mArtistTopHits + = new ConcurrentHashMap<>(); + + public HatchetCollection() { + super(TomahawkApp.PLUGINNAME_HATCHET, ""); + } + + @Override + public void loadIcon(ImageView imageView, boolean grayOut) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_hatchet, grayOut ? R.color.disabled_resolver : 0); + } + + public void wipe() { + mArtists.clear(); + mAlbums.clear(); + mAlbumTracks.clear(); + mArtistAlbums.clear(); + } + + @Override + public Promise getQueries(int sortMode) { + return null; + } + + public void addArtist(Artist artist) { + mArtists.add(artist); + } + + @Override + public Promise, Throwable, Void> getArtists(int sortMode) { + final Deferred, Throwable, Void> deferred + = new ADeferredObject<>(); + Comparator comparator = null; + switch (sortMode) { + case SORT_ALPHA: + comparator = new AlphaComparator(); + break; + case SORT_LAST_MODIFIED: + comparator = new AlphaComparator(); //TODO + break; + } + List artists = new ArrayList<>(mArtists); + if (comparator != null) { + Collections.sort(artists, comparator); + } + CollectionCursor collectionCursor = new CollectionCursor<>(artists, Artist.class); + return deferred.resolve(collectionCursor); + } + + public void addAlbumArtist(Artist artist) { + mAlbumArtists.add(artist); + } + + @Override + public Promise, Throwable, Void> getAlbumArtists(int sortMode) { + final Deferred, Throwable, Void> deferred + = new ADeferredObject<>(); + Comparator comparator = null; + switch (sortMode) { + case SORT_ALPHA: + comparator = new AlphaComparator(); + break; + case SORT_LAST_MODIFIED: + comparator = new AlphaComparator(); //TODO + break; + } + List artists = new ArrayList<>(mAlbumArtists); + if (comparator != null) { + Collections.sort(artists, comparator); + } + CollectionCursor collectionCursor = new CollectionCursor<>(artists, Artist.class); + return deferred.resolve(collectionCursor); + } + + public void addAlbum(Album album) { + mAlbums.add(album); + } + + @Override + public Promise, Throwable, Void> getAlbums(int sortMode) { + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + Comparator comparator = null; + switch (sortMode) { + case SORT_ALPHA: + comparator = new AlphaComparator(); + break; + case SORT_ARTIST_ALPHA: + comparator = new ArtistAlphaComparator(); + break; + case SORT_LAST_MODIFIED: + comparator = new AlphaComparator(); //TODO + break; + } + List albums = new ArrayList<>(mAlbums); + if (comparator != null) { + Collections.sort(albums, comparator); + } + CollectionCursor collectionCursor = new CollectionCursor<>(albums, Album.class); + return deferred.resolve(collectionCursor); + } + + public void addArtistAlbums(Artist artist, List albums) { + Collections.sort(albums, new AlphaComparator()); + mArtistAlbums.put(artist, albums); + } + + @Override + public Promise, Throwable, Void> getArtistAlbums(final Artist artist) { + final Deferred, Throwable, Void> deferred = new ADeferredObject<>(); + CollectionCursor collectionCursor = null; + if (mArtistAlbums.get(artist) != null) { + List albums = new ArrayList<>(); + albums.addAll(mArtistAlbums.get(artist)); + collectionCursor = new CollectionCursor<>(albums, Album.class); + } + return deferred.resolve(collectionCursor); + } + + @Override + public Promise getArtistTracks(Artist artist) { + Deferred deferred = new ADeferredObject<>(); + return deferred.resolve(null); + } + + public void addAlbumTracks(Album album, Playlist playlist) { + mAlbumTracks.put(album, playlist); + } + + @Override + public Promise getAlbumTracks(final Album album) { + Deferred deferred = new ADeferredObject<>(); + return deferred.resolve(mAlbumTracks.get(album)); + } + + @Override + public Promise getAlbumTrackCount(final Album album) { + Deferred deferred = new ADeferredObject<>(); + if (mAlbumTracks.get(album) != null) { + return deferred.resolve(mAlbumTracks.get(album).size()); + } + return deferred + .reject(new Throwable("Couldn't find album " + album.getName() + " in collection")); + } + + public void addArtistTopHits(Artist artist, Playlist playlist) { + mArtistTopHits.put(artist, playlist); + } + + /** + * @return A {@link java.util.List} of all top hits {@link Track}s from the given Artist. + */ + public Promise getArtistTopHits(final Artist artist) { + Deferred deferred = new ADeferredObject<>(); + return deferred.resolve(mArtistTopHits.get(artist)); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Image.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Image.java new file mode 100644 index 000000000..2bcc66d40 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Image.java @@ -0,0 +1,136 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.content.res.Resources; +import android.util.DisplayMetrics; + +/** + * Class which represents a Tomahawk {@link org.tomahawk.libtomahawk.collection.Image}. + */ +public class Image extends Cacheable { + + private static final float IMAGE_SIZE_SMALL = 0.2f; + + private static final float IMAGE_SIZE_LARGE = 0.5f; + + private static int sScreenHeightPixels = 0; + + private static int sScreenWidthPixels = 0; + + private final String mImagePath; + + private final boolean mIsHatchetImage; + + private int mWidth = -1; + + private int mHeight = -1; + + /** + * Construct a new {@link org.tomahawk.libtomahawk.collection.Image} + */ + private Image(String imagePath, boolean isHatchetImage) { + super(Image.class, getCacheKey(imagePath)); + + mImagePath = imagePath; + mIsHatchetImage = isHatchetImage; + } + + /** + * Construct a new {@link org.tomahawk.libtomahawk.collection.Image} + */ + private Image(String imagePath, boolean isHatchetImage, int width, int height) { + super(Image.class, getCacheKey(imagePath)); + + mImagePath = imagePath; + mIsHatchetImage = isHatchetImage; + mWidth = width; + mHeight = height; + } + + /** + * Returns the {@link org.tomahawk.libtomahawk.collection.Image} with the given image path and + * boolean to determine whether or not this image should be scaled down. If none exists in our + * static {@link java.util.concurrent.ConcurrentHashMap} yet, construct and add it. + */ + public static Image get(String imagePath, boolean scaleItDown) { + Cacheable cacheable = get(Image.class, getCacheKey(imagePath)); + return cacheable != null ? (Image) cacheable : new Image(imagePath, scaleItDown); + } + + /** + * Returns the {@link org.tomahawk.libtomahawk.collection.Image} with the given image path and + * boolean to determine whether or not this image should be scaled down. If none exists in our + * static {@link java.util.concurrent.ConcurrentHashMap} yet, construct and add it. + */ + public static Image get(String imagePath, boolean scaleItDown, int width, int height) { + Cacheable cacheable = get(Image.class, getCacheKey(imagePath)); + return cacheable != null ? (Image) cacheable + : new Image(imagePath, scaleItDown, width, height); + } + + public static Image getByKey(String id) { + return (Image) get(Image.class, id); + } + + public String getImagePath() { + return mImagePath; + } + + public boolean isHatchetImage() { + return mIsHatchetImage; + } + + public int getHeight() { + return mHeight; + } + + public int getWidth() { + return mWidth; + } + + public static int getSmallImageSize() { + getScreenResolution(); + return (int) (sScreenHeightPixels * IMAGE_SIZE_SMALL); + } + + public static int getLargeImageSize() { + getScreenResolution(); + return (int) (sScreenHeightPixels * IMAGE_SIZE_LARGE); + } + + private static void getScreenResolution() { + if (sScreenWidthPixels == 0 || sScreenHeightPixels == 0) { + Resources resources = TomahawkApp.getContext().getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + int width; + int height; + if (metrics.widthPixels > metrics.heightPixels) { + width = metrics.widthPixels; + height = metrics.heightPixels; + } else { + width = metrics.heightPixels; + height = metrics.widthPixels; + } + sScreenWidthPixels = width; + sScreenHeightPixels = height; + } + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/LastModifiedComparator.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/LastModifiedComparator.java new file mode 100644 index 000000000..0422127db --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/LastModifiedComparator.java @@ -0,0 +1,67 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is used to compare two {@link Object}s. + */ +public class LastModifiedComparator implements Comparator { + + private Map mTimeStampMap; + + /** + * Construct this {@link LastModifiedComparator} + * + * @param timeStampMap the ConcurrentHashMap used to determine the timeStamps of the + * TomahawkListItems which will be sorted + */ + public LastModifiedComparator(Map timeStampMap) { + mTimeStampMap = new HashMap<>(timeStampMap); + } + + /** + * The actual comparison method + * + * @param o1 First object to compare + * @param o2 Second object to compare + * @return int containing comparison score + */ + @Override + public int compare(T o1, T o2) { + Long a1TimeStamp = mTimeStampMap.get(o1); + if (a1TimeStamp == null) { + a1TimeStamp = 0L; + } + Long a2TimeStamp = mTimeStampMap.get(o2); + if (a2TimeStamp == null) { + a2TimeStamp = 0L; + } + + if (a1TimeStamp > a2TimeStamp) { + return -1; + } else if (a1TimeStamp < a2TimeStamp) { + return 1; + } else { + return 0; + } + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemDrawable.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemDrawable.java new file mode 100644 index 000000000..82cb65b52 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemDrawable.java @@ -0,0 +1,37 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +/** + * This class represents an {@link ListItemDrawable}. + */ +public class ListItemDrawable { + + private final int mResourceId; + + /** + * Construct a new {@link ListItemDrawable} with the given resource id + */ + public ListItemDrawable(int resourceId) { + mResourceId = resourceId; + } + + public int getResourceId() { + return mResourceId; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemString.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemString.java new file mode 100644 index 000000000..dd73c0c2f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/ListItemString.java @@ -0,0 +1,56 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +/** + * This class represents an {@link ListItemString}. + */ +public class ListItemString { + + private final String mText; + + private boolean mHighlighted; + + /** + * Construct a new {@link ListItemString} with the given text + */ + public ListItemString(String text, boolean highlighted) { + this(text); + + mHighlighted = highlighted; + } + + /** + * Construct a new {@link ListItemString} with the given text + */ + public ListItemString(String text) { + if (text == null) { + mText = ""; + } else { + mText = text; + } + } + + public String getText() { + return mText; + } + + public boolean isHighlighted() { + return mHighlighted; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Playlist.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Playlist.java new file mode 100644 index 000000000..c3968fc9f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Playlist.java @@ -0,0 +1,476 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A {@link Playlist} is a {@link org.tomahawk.libtomahawk.collection.Playlist} created by the user + * and stored in the database + */ +public class Playlist extends Cacheable implements AlphaComparable { + + private static final String TAG = Playlist.class.getSimpleName(); + + private String mName = ""; + + private CollectionCursor mCursor = null; + + private List mAddedEntries = new ArrayList<>(); + + private Map mCachedEntries = new HashMap<>(); + + private List mIndex = new ArrayList<>(); + + private List mShuffledIndex = new ArrayList<>(); + + private static class Index { + + protected Index(int internalIndex, boolean fromMergedItems) { + this.internalIndex = internalIndex; + this.fromMergedItems = fromMergedItems; + } + + int internalIndex; + + boolean fromMergedItems; + } + + private String mId; + + private String mHatchetId; + + private String mCurrentRevision = ""; + + private String[] mTopArtistNames; + + private long mCount = -1; + + private boolean mIsFilled; + + private String mUserId; + + /** + * Construct a new empty {@link Playlist}. + */ + protected Playlist(String id) { + super(Playlist.class, id); + + mId = id; + } + + public Playlist copy(Playlist destination) { + destination.mName = mName; + destination.mCursor = mCursor.copy(); + for (PlaylistEntry entry : mAddedEntries) { + destination.mAddedEntries.add(entry); + } + for (PlaylistEntry key : mCachedEntries.keySet()) { + destination.mCachedEntries.put(key, mCachedEntries.get(key)); + } + for (Index index : mIndex) { + destination.mIndex.add(index); + } + for (Index index : mShuffledIndex) { + destination.mShuffledIndex.add(index); + } + destination.mHatchetId = mHatchetId; + destination.mCurrentRevision = mCurrentRevision; + if (mTopArtistNames != null) { + destination.mTopArtistNames = mTopArtistNames.clone(); + } else { + destination.mTopArtistNames = null; + } + destination.mCount = mCount; + destination.mIsFilled = mIsFilled; + destination.mUserId = mUserId; + return destination; + } + + /** + * Returns the {@link Playlist} with the given parameters. If none exists in our static {@link + * ConcurrentHashMap} yet, construct and add it. + * + * @return {@link Playlist} with the given parameters + */ + public static Playlist get(String id) { + Cacheable cacheable = get(Playlist.class, id); + return cacheable != null ? (Playlist) cacheable : new Playlist(id); + } + + /** + * Create a {@link Playlist} from a list of {@link PlaylistEntry}s. + * + * @return a reference to the constructed {@link Playlist} + */ + public static Playlist fromEntryList(String id, String name, String currentRevision, + List entries) { + CollectionCursor cursor = + new CollectionCursor<>(entries, PlaylistEntry.class); + return fromCursor(id, name, currentRevision, cursor); + } + + /** + * Create a {@link Playlist} from a list of {@link PlaylistEntry}s. + * + * @return a reference to the constructed {@link Playlist} + */ + public static Playlist fromEmptyList(String id, String name) { + CollectionCursor cursor = + new CollectionCursor<>(new ArrayList(), PlaylistEntry.class); + return fromCursor(id, name, null, cursor); + } + + /** + * Create a {@link Playlist} from a list of {@link org.tomahawk.libtomahawk.resolver.Query}s. + * + * @return a reference to the constructed {@link Playlist} + */ + public static Playlist fromQueryList(String id, String name, String currentRevision, + List queries) { + List entries = new ArrayList<>(); + for (Query query : queries) { + entries.add(PlaylistEntry.get(id, query, + IdGenerator.getLifetimeUniqueStringId())); + } + CollectionCursor cursor = + new CollectionCursor<>(entries, PlaylistEntry.class); + return fromCursor(id, name, currentRevision, cursor); + } + + /** + * Create a {@link Playlist} from a {@link CollectionCursor} containing {@link PlaylistEntry}s. + * + * @return a reference to the constructed {@link Playlist} + */ + private static Playlist fromCursor(String id, String name, String currentRevision, + CollectionCursor cursor) { + Playlist pl = Playlist.get(id); + pl.setName(name); + pl.setCurrentRevision(currentRevision); + pl.setCursor(cursor); + return pl; + } + + public void setCursor(CollectionCursor cursor) { + mCursor = cursor; + initIndex(); + } + + private void initIndex() { + mAddedEntries.clear(); + mCachedEntries.clear(); + mIndex.clear(); + mShuffledIndex.clear(); + for (int i = 0; i < mCursor.size(); i++) { + mIndex.add(new Index(i, false)); + } + } + + /** + * Get the {@link org.tomahawk.libtomahawk.collection.Playlist} by providing its cache key. Only + * use this for playlists that are not stored in the database! + */ + public static Playlist getByKey(String id) { + return (Playlist) get(Playlist.class, id); + } + + public String getId() { + return mId; + } + + public String getHatchetId() { + return mHatchetId; + } + + public void setHatchetId(String hatchetId) { + mHatchetId = hatchetId; + } + + public void setCurrentRevision(String currentRevision) { + mCurrentRevision = currentRevision == null ? "" : currentRevision; + } + + public String getCurrentRevision() { + return mCurrentRevision; + } + + public String[] getTopArtistNames() { + return mTopArtistNames; + } + + public void setTopArtistNames(String[] topArtistNames) { + mTopArtistNames = topArtistNames; + } + + public void updateTopArtistNames(boolean getMostRecentArtists) { + String[] results; + if (getMostRecentArtists) { + List artistNames = new ArrayList<>(); + for (int i = 0; i < size() && i < 5; i++) { + artistNames.add(getArtistName(i)); + } + results = artistNames.toArray(new String[artistNames.size()]); + } else { + final HashMap countMap = new HashMap<>(); + for (int i = 0; i < size(); i++) { + String artistName = getArtistName(i); + if (countMap.containsKey(artistName)) { + countMap.put(artistName, countMap.get(artistName) + 1); + } else { + countMap.put(artistName, 1); + } + } + results = new String[0]; + if (countMap.size() > 0) { + PriorityQueue topArtistNames = new PriorityQueue<>(countMap.size(), + new Comparator() { + @Override + public int compare(String lhs, String rhs) { + return countMap.get(lhs) >= countMap.get(rhs) ? -1 : 1; + } + } + ); + topArtistNames.addAll(countMap.keySet()); + results = topArtistNames.toArray(new String[topArtistNames.size()]); + } + } + mTopArtistNames = results; + } + + /** + * @return this {@link Playlist}'s name + */ + public String getName() { + return mName; + } + + /** + * Set the name of this {@link Playlist} + * + * @param name the name to be set + */ + public void setName(String name) { + mName = name == null ? "" : name; + } + + private PlaylistEntry getEntry(Index index) { + PlaylistEntry entry; + if (index.fromMergedItems) { + entry = mAddedEntries.get(index.internalIndex); + } else { + entry = mCursor.get(index.internalIndex); + } + mCachedEntries.put(entry, index); + return entry; + } + + /** + * Return the current count of entries in the {@link Playlist} + */ + public int size() { + return mIndex.size(); + } + + /** + * Return all PlaylistEntries in the {@link Playlist}. This is a very costly operation and + * should only be done if absolutely necessary. Consider using {@link #getEntryAtPos(int)}. + */ + public List getEntries() { + return getEntries(false); + } + + /** + * Return all PlaylistEntries in the {@link Playlist}. This is a very costly operation and + * should only be done if absolutely necessary. Consider using {@link #getEntryAtPos(int)}. + */ + public List getEntries(boolean shuffled) { + List entries = new ArrayList<>(); + List indexList = shuffled ? mShuffledIndex : mIndex; + for (Index index : indexList) { + PlaylistEntry entry = getEntry(index); + entries.add(entry); + mCachedEntries.put(entry, index); + } + return entries; + } + + /** + * Add the given {@link Query} to this {@link Playlist} + * + * @param position the position at which to insert the given {@link Query} + * @param query the {@link Query} to add + * @return the {@link PlaylistEntry} that got created and added to this {@link Playlist} + */ + public PlaylistEntry addQuery(int position, Query query) { + PlaylistEntry entry = PlaylistEntry.get(mId, query, + IdGenerator.getLifetimeUniqueStringId()); + mAddedEntries.add(entry); + Index index = new Index(mAddedEntries.size() - 1, true); + mIndex.add(position, index); + mCachedEntries.put(entry, index); + return entry; + } + + /** + * Remove the given {@link Query} from this playlist + */ + public boolean deleteEntry(PlaylistEntry entry) { + Index index = mCachedEntries.get(entry); + if (index == null) { + Log.d(TAG, "deleteEntry - couldn't find cached PlaylistEntry."); + } + return mIndex.remove(index); + } + + public long getCount() { + return mCount; + } + + public void setCount(long count) { + mCount = count; + } + + public boolean isFilled() { + return mIsFilled; + } + + public void setFilled(boolean isFilled) { + mIsFilled = isFilled; + } + + public PlaylistEntry getEntryAtPos(int position) { + return getEntryAtPos(position, false); + } + + public PlaylistEntry getEntryAtPos(int position, boolean shuffled) { + List indexList = shuffled ? mShuffledIndex : mIndex; + if (position < 0 || position >= indexList.size()) { + return null; + } + Index index = indexList.get(position); + return getEntry(index); + } + + public int getIndexOfEntry(PlaylistEntry entry) { + return getIndexOfEntry(entry, false); + } + + public int getIndexOfEntry(PlaylistEntry entry, boolean shuffled) { + Index index = mCachedEntries.get(entry); + List indexList = shuffled ? mShuffledIndex : mIndex; + return indexList.indexOf(index); + } + + public boolean containsEntry(PlaylistEntry entry) { + return getIndexOfEntry(entry) >= 0; + } + + public String getUserId() { + return mUserId; + } + + public void setUserId(String userId) { + mUserId = userId; + } + + public boolean allFromOneArtist() { + if (size() < 2) { + return true; + } + String artistname = getArtistName(0); + for (int i = 1; i < size(); i++) { + String artistNameToCompare = getArtistName(i); + if (!artistNameToCompare.equals(artistname)) { + return false; + } + artistname = artistNameToCompare; + } + return true; + } + + public String getArtistName(int position) { + Index index = mIndex.get(position); + if (index.fromMergedItems) { + return mAddedEntries.get(index.internalIndex).getArtist().getName(); + } else { + return mCursor.getArtistName(index.internalIndex); + } + } + + /** + * Shuffle this {@link Playlist}'s tracks. This method ensures that there's always a minimum + * amount of tracks in sequence that have the same artist. + * + * @param currentIndex the track at this position will be put at the top of the resulting + * shuffled list of tracks. + */ + public void buildShuffledIndex(int currentIndex) throws IndexOutOfBoundsException { + mShuffledIndex.clear(); + if (currentIndex >= 0) { + // Add the current entry to the top of shuffled index + mShuffledIndex.add(mIndex.get(currentIndex)); + } + List artistNames = new ArrayList<>(); + Map> artistsTrackIndexes = new HashMap<>(); + for (int i = 0; i < size(); i++) { + if (i != currentIndex) { // Don't add the currently playing track + String artistName = getArtistName(i); + if (artistsTrackIndexes.get(artistName) == null) { + artistsTrackIndexes.put(artistName, new ArrayList()); + artistNames.add(artistName); + } + artistsTrackIndexes.get(artistName).add(i); + } + } + Collections.shuffle(artistNames); + while (artistNames.size() > 0) { + for (int i = 0; i < artistNames.size(); i++) { + String artistName = artistNames.get(i); + // Now we can get the list of track indexes + List indexes = artistsTrackIndexes.get(artistName); + int randomPos = (int) (Math.random() * indexes.size()); + // Add the randomly picked track index to our shuffled index + int shuffledIndex = indexes.remove(randomPos); + mShuffledIndex.add(mIndex.get(shuffledIndex)); + if (indexes.size() == 0) { + artistNames.remove(i); + } + } + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( id: " + getId() + ", hatchetId: " + mHatchetId + + ", name: " + getName() + ", size: " + size() + " )@" + + Integer.toHexString(hashCode()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistComparator.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistComparator.java new file mode 100644 index 000000000..497574338 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistComparator.java @@ -0,0 +1,51 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import java.util.Comparator; + +/** + * This class is used to compare two {@link Playlist}s. + */ +public class PlaylistComparator implements Comparator { + + /** + * Construct this {@link PlaylistComparator} + */ + public PlaylistComparator() { + } + + /** + * The actual comparison method + * + * @param p1 First Playlist to compare + * @param p2 Second Playlist to compare + * @return int containing comparison score + */ + @Override + public int compare(Playlist p1, Playlist p2) { + int score = p1.getName().compareTo(p2.getName()); + if (score == 0) { + score = p1.getId().compareTo(p2.getId()); + if (score == 0 && p1.getHatchetId() != null && p2.getHatchetId() != null) { + score = p1.getHatchetId().compareTo(p2.getHatchetId()); + } + } + return score; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistEntry.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistEntry.java new file mode 100644 index 000000000..577493bcc --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/PlaylistEntry.java @@ -0,0 +1,88 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.tomahawk.libtomahawk.resolver.Query; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class represents a entry in a playlist. It is needed because we need to be able to + * distinguish for example between two same queries in a Playlist. + */ +public class PlaylistEntry extends Cacheable { + + private final String mId; + + private final Query mQuery; + + private final String mPlaylistId; + + /** + * Construct a new {@link org.tomahawk.libtomahawk.collection.PlaylistEntry} + */ + private PlaylistEntry(String playlistId, Query query, String entryId) { + super(PlaylistEntry.class, getCacheKey(playlistId, entryId)); + + mPlaylistId = playlistId; + mQuery = query; + mId = entryId; + } + + /** + * Returns the {@link PlaylistEntry} with the given parameters. If none exists in our static + * {@link ConcurrentHashMap} yet, construct and add it. + * + * @return {@link PlaylistEntry} with the given parameters + */ + public static PlaylistEntry get(String playlistId, Query query, String entryId) { + Cacheable cacheable = get(PlaylistEntry.class, getCacheKey(playlistId, entryId)); + return cacheable != null ? (PlaylistEntry) cacheable + : new PlaylistEntry(playlistId, query, entryId); + } + + public static PlaylistEntry getByKey(String cacheKey) { + return (PlaylistEntry) get(PlaylistEntry.class, cacheKey); + } + + public String getId() { + return mId; + } + + public String getPlaylistId() { + return mPlaylistId; + } + + public Query getQuery() { + return mQuery; + } + + public Artist getArtist() { + return mQuery.getArtist(); + } + + public Album getAlbum() { + return mQuery.getAlbum(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( playlistId: " + mPlaylistId + ", entryId: " + mId + + ", " + mQuery.toShortString() + " )@" + Integer.toHexString(hashCode()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/ScriptResolverCollection.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/ScriptResolverCollection.java new file mode 100644 index 000000000..505092cfd --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/ScriptResolverCollection.java @@ -0,0 +1,136 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.Deferred; +import org.jdeferred.DoneCallback; +import org.jdeferred.DonePipe; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptJob; +import org.tomahawk.libtomahawk.resolver.ScriptObject; +import org.tomahawk.libtomahawk.resolver.ScriptPlugin; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverCollectionMetaData; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.widget.ImageView; + +/** + * This class represents a Collection which contains tracks/albums/artists retrieved by a + * ScriptResolver. + */ +public class ScriptResolverCollection extends DbCollection implements ScriptPlugin { + + private final static String TAG = ScriptResolverCollection.class.getSimpleName(); + + private ScriptObject mScriptObject; + + private ScriptAccount mScriptAccount; + + private ScriptResolverCollectionMetaData mMetaData; + + public ScriptResolverCollection(ScriptObject object, ScriptAccount account) { + super(account.getScriptResolver()); + + mScriptObject = object; + mScriptAccount = account; + } + + @Override + public Promise getCollectionId() { + final Deferred deferred + = new ADeferredObject<>(); + if (mMetaData == null) { + ScriptJob.start(mScriptObject, "settings", + new ScriptJob.ResultsCallback( + ScriptResolverCollectionMetaData.class) { + @Override + public void onReportResults(ScriptResolverCollectionMetaData results) { + mMetaData = results; + deferred.resolve(results); + } + }, new ScriptJob.FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + deferred.reject(new Throwable(errormessage)); + } + }); + return deferred.then( + new DonePipe() { + @Override + public Promise pipeDone( + ScriptResolverCollectionMetaData result) { + final Deferred deferred + = new ADeferredObject<>(); + return deferred.resolve(result.id); + } + }); + } else { + Deferred d = new ADeferredObject<>(); + return d.resolve(mMetaData.id); + } + } + + public Deferred getMetaData() { + final Deferred deferred + = new ADeferredObject<>(); + if (mMetaData == null) { + ScriptJob.start(mScriptObject, "settings", + new ScriptJob.ResultsCallback( + ScriptResolverCollectionMetaData.class) { + @Override + public void onReportResults(ScriptResolverCollectionMetaData results) { + mMetaData = results; + deferred.resolve(results); + } + }, new ScriptJob.FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + deferred.reject(new Throwable(errormessage)); + } + }); + } else { + deferred.resolve(mMetaData); + } + return deferred; + } + + @Override + public ScriptObject getScriptObject() { + return mScriptObject; + } + + @Override + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + @Override + public void loadIcon(final ImageView imageView, final boolean grayOut) { + getMetaData().done(new DoneCallback() { + @Override + public void onDone(ScriptResolverCollectionMetaData result) { + String completeIconPath = mScriptAccount.getPath() + "/content/" + result.iconfile; + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + completeIconPath); + } + }); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/StationPlaylist.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/StationPlaylist.java new file mode 100644 index 000000000..35ffede85 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/StationPlaylist.java @@ -0,0 +1,411 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.jdeferred.AlwaysCallback; +import org.jdeferred.Deferred; +import org.jdeferred.DoneCallback; +import org.jdeferred.FailCallback; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGenerator; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorManager; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorResult; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorSearchResult; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.GsonHelper; + +import android.support.v4.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class StationPlaylist extends Playlist { + + private String mSessionId; + + private Playlist mPlaylist; + + private List> mArtists; + + private List> mTracks; + + private List mGenres; + + private final List mCandidates = new ArrayList<>(); + + private long mCreatedTimeStamp = 0L; + + private long mPlayedTimeStamp = 0L; + + private Deferred, Throwable, Void> mFillDeferred; + + private StationPlaylist(List> artists, List> tracks, + List genres) { + super(getCacheKey(artists, tracks, genres)); + + mArtists = artists; + mTracks = tracks; + mGenres = genres; + + String name = ""; + if (mArtists != null) { + for (Pair artist : mArtists) { + if (!name.isEmpty()) { + name += ", "; + } + name += artist.first.getPrettyName(); + } + } + if (mTracks != null) { + for (Pair track : mTracks) { + if (!name.isEmpty()) { + name += ", "; + } + name += track.first.getArtist().getPrettyName() + " - " + track.first.getName(); + } + } + if (mGenres != null) { + for (String genre : mGenres) { + if (!name.isEmpty()) { + name += ", "; + } + name += genre; + } + } + setName(name); + + mCreatedTimeStamp = System.currentTimeMillis(); + } + + private StationPlaylist(Playlist playlist) { + super(getCacheKey(playlist)); + + mPlaylist = playlist; + + setName(mPlaylist.getName()); + + mCreatedTimeStamp = System.currentTimeMillis(); + } + + private static String getCacheKey(Playlist playlist) { + return "station_" + playlist.getCacheKey(); + } + + private static String getCacheKey(List> artists, + List> tracks, List genres) { + String key = "station_"; + if (artists != null) { + Collections.sort(artists, new Comparator>() { + @Override + public int compare(Pair lhs, Pair rhs) { + return lhs.first.getName().compareToIgnoreCase(rhs.first.getName()); + } + }); + for (Pair artist : artists) { + key += "♠" + artist.first.getCacheKey(); + } + } + if (tracks != null) { + Collections.sort(tracks, new Comparator>() { + @Override + public int compare(Pair lhs, Pair rhs) { + return lhs.first.getName().compareToIgnoreCase(rhs.first.getName()); + } + }); + for (Pair track : tracks) { + key += "♠" + track.first.getCacheKey(); + } + } + if (genres != null) { + Collections.sort(genres); + for (String genre : genres) { + key += "♠" + genre; + } + } + return key; + } + + public static StationPlaylist get(String json) { + JsonObject jsonObject = GsonHelper.get().fromJson(json, JsonObject.class); + + List> artists = null; + if (jsonObject.has("artists") && jsonObject.get("artists").isJsonArray()) { + artists = new ArrayList<>(); + for (JsonElement element : jsonObject.getAsJsonArray("artists")) { + if (element.isJsonObject()) { + JsonObject o = element.getAsJsonObject(); + Artist artist = Artist.get(o.get("artist").getAsString()); + JsonElement idElement = o.get(getIdKey()); + String id = ""; + if (idElement != null) { + id = idElement.getAsString(); + } + artists.add(new Pair<>(artist, id)); + } + } + } + + List> tracks = null; + if (jsonObject.has("tracks") && jsonObject.get("tracks").isJsonArray()) { + tracks = new ArrayList<>(); + for (JsonElement element : jsonObject.getAsJsonArray("tracks")) { + if (element.isJsonObject()) { + JsonObject o = element.getAsJsonObject(); + Artist artist = Artist.get(o.get("artist").getAsString()); + Album album = Album.get(o.get("album").getAsString(), artist); + Track track = Track.get(o.get("track").getAsString(), album, artist); + JsonElement idElement = o.get(getIdKey()); + String id = ""; + if (idElement != null) { + id = idElement.getAsString(); + } + tracks.add(new Pair<>(track, id)); + } + } + } + + List genres = null; + if (jsonObject.has("genres") && jsonObject.get("genres").isJsonArray()) { + genres = new ArrayList<>(); + for (JsonElement element : jsonObject.getAsJsonArray("genres")) { + if (element.isJsonObject()) { + JsonObject o = element.getAsJsonObject(); + genres.add(o.get("name").getAsString()); + } + } + } + + return get(artists, tracks, genres); + } + + public static StationPlaylist get(List> artists, + List> tracks, List genres) { + Cacheable cacheable = get(Playlist.class, getCacheKey(artists, tracks, genres)); + return cacheable != null ? (StationPlaylist) cacheable + : new StationPlaylist(artists, tracks, genres); + } + + public static StationPlaylist get(Playlist playlist) { + Cacheable cacheable = get(Playlist.class, getCacheKey(playlist)); + return cacheable != null ? (StationPlaylist) cacheable : new StationPlaylist(playlist); + } + + public List> getArtists() { + return mArtists; + } + + public List> getTracks() { + return mTracks; + } + + public List getGenres() { + return mGenres; + } + + public Playlist getPlaylist() { + return mPlaylist; + } + + public void setCreatedTimeStamp(long createdTimeStamp) { + mCreatedTimeStamp = createdTimeStamp; + } + + public long getCreatedTimeStamp() { + return mCreatedTimeStamp; + } + + public long getPlayedTimeStamp() { + return mPlayedTimeStamp; + } + + public void setPlayedTimeStamp(long playedTimeStamp) { + mPlayedTimeStamp = playedTimeStamp; + } + + public Promise, Throwable, Void> fillPlaylist(final int limit) { + if (mFillDeferred != null && mFillDeferred.isPending()) { + return null; + } + final Deferred, Throwable, Void> fillDeferred = new ADeferredObject<>(); + mFillDeferred = fillDeferred; + pickSeedsFromPlaylist().done(new DoneCallback() { + @Override + public void onDone(Void result) { + ScriptPlaylistGenerator generator = + ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator(); + if (mCandidates.size() >= limit) { + // We got enough candidates in cache + List queries = new ArrayList<>(); + for (int i = 0; i < limit; i++) { + queries.add(mCandidates.remove(0)); + } + fillDeferred.resolve(queries); + } else if (generator != null) { + generator.fillPlaylist(mSessionId, mArtists, mTracks, mGenres) + .done(new DoneCallback() { + @Override + public void onDone(ScriptPlaylistGeneratorResult result) { + mSessionId = result.sessionId; + List queries = new ArrayList<>(); + if (result.results != null) { + int actualLimit = Math.min(result.results.size(), limit); + for (int i = 0; i < actualLimit; i++) { + queries.add(result.results.remove(0)); + } + // Add the rest to our candidate cache + mCandidates.addAll(result.results); + } + if (queries.size() == 0) { + fillDeferred.reject( + new Throwable("Couldn't find suitable tracks")); + } else { + fillDeferred.resolve(queries); + } + } + }) + .fail(new FailCallback() { + @Override + public void onFail(Throwable result) { + fillDeferred.reject(result); + } + }); + } + } + }); + return mFillDeferred; + } + + public String toJson() { + JsonObject json = new JsonObject(); + + if (mArtists != null) { + JsonArray artists = new JsonArray(); + for (Pair artist : mArtists) { + JsonObject o = new JsonObject(); + o.addProperty("artist", artist.first.getName()); + o.addProperty(getIdKey(), artist.second); + artists.add(o); + } + json.add("artists", artists); + } + + if (mTracks != null) { + JsonArray tracks = new JsonArray(); + for (Pair track : mTracks) { + JsonObject o = new JsonObject(); + o.addProperty("track", track.first.getName()); + o.addProperty("artist", track.first.getArtist().getName()); + o.addProperty("album", track.first.getAlbum().getName()); + o.addProperty(getIdKey(), track.second); + tracks.add(o); + } + json.add("tracks", tracks); + } + + if (mGenres != null) { + JsonArray genres = new JsonArray(); + for (String genre : mGenres) { + JsonObject o = new JsonObject(); + o.addProperty("name", genre); + genres.add(o); + } + json.add("genres", genres); + } + + return GsonHelper.get().toJson(json); + } + + private Promise pickSeedsFromPlaylist() { + ADeferredObject deferred = new ADeferredObject<>(); + if (mPlaylist == null || mTracks != null) { + // No need to do anything + deferred.resolve(null); + } else if (mPlaylist.size() < 5) { + // No need to pick random tracks, we use all of them anyways + List> tracks = new ArrayList<>(); + for (PlaylistEntry entry : mPlaylist.getEntries()) { + // Let the js resolver fetch the track ids for us + tracks.add(new Pair<>(entry.getQuery().getBasicTrack(), "")); + } + mTracks = tracks; + deferred.resolve(null); + } else { + pickSeedsFromPlaylist(deferred, new ArrayList(), + new ArrayList>(), 10); + } + return deferred; + } + + private void pickSeedsFromPlaylist(final ADeferredObject deferred, + final List pickedIndexes, final List> tracks, + int attemptCount) { + if (attemptCount-- < 0 || tracks.size() >= 5) { + mTracks = tracks; + deferred.resolve(null); + return; + } + int size = mPlaylist.size(); + int randomIndex = (int) (Math.random() * size); + while (pickedIndexes.contains(randomIndex)) { + if (randomIndex + 1 < size) { + randomIndex++; + } else { + randomIndex = 0; + } + } + pickedIndexes.add(randomIndex); + PlaylistEntry entry = mPlaylist.getEntryAtPos(randomIndex); + Track candidate = entry.getQuery().getBasicTrack(); + final int finalAttemptCount = attemptCount; + ScriptPlaylistGenerator generator = + ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator(); + if (generator != null) { + generator.search("track:" + candidate.getName() + + "%20artist:" + candidate.getArtist().getName()).always( + new AlwaysCallback() { + @Override + public void onAlways(Promise.State state, + ScriptPlaylistGeneratorSearchResult resolved, Throwable rejected) { + if (resolved != null && resolved.mTracks != null + && resolved.mTracks.size() > 0) { + tracks.add(resolved.mTracks.get(0)); + } + pickSeedsFromPlaylist(deferred, pickedIndexes, tracks, + finalAttemptCount); + } + }); + } + } + + private static String getIdKey() { + return ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGeneratorId() + "_id"; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( id: " + getId() + ", name: " + getName() + + ", size: " + size() + " )@" + Integer.toHexString(hashCode()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/TomahawkComparable.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/TomahawkComparable.java new file mode 100644 index 000000000..442eb48e3 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/TomahawkComparable.java @@ -0,0 +1,22 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +public interface TomahawkComparable { + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/Track.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/Track.java new file mode 100644 index 000000000..76abfc97e --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/Track.java @@ -0,0 +1,166 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import android.text.TextUtils; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class represents a {@link Track}. + */ +public class Track extends Cacheable implements AlphaComparable, ArtistAlphaComparable { + + private final String mName; + + private final Album mAlbum; + + private final Artist mArtist; + + private long mDuration; + + private int mYear; + + private int mAlbumPos; + + private int mDiscNumber; + + /** + * Construct a new {@link Track} + */ + private Track(String trackName, Album album, Artist artist) { + super(Track.class, getCacheKey(trackName, album.getName(), artist.getName())); + + mName = trackName != null ? trackName : ""; + mAlbum = album; + mArtist = artist; + } + + /** + * Returns the {@link Track} with the given id. If none exists in our static {@link + * ConcurrentHashMap} yet, construct and add it. + * + * @return {@link Track} with the given id + */ + public static Track get(String trackName, Album album, Artist artist) { + Cacheable cacheable = get(Track.class, + getCacheKey(trackName, album.getName(), artist.getName())); + return cacheable != null ? (Track) cacheable : new Track(trackName, album, artist); + } + + public static Track getByKey(String cacheKey) { + return (Track) get(Track.class, cacheKey); + } + + /** + * @return the {@link Track}'s name + */ + public String getName() { + return mName; + } + + /** + * @return the {@link Track}'s {@link Artist} + */ + public Artist getArtist() { + return mArtist; + } + + /** + * @return the {@link Track}'s {@link Album} + */ + public Album getAlbum() { + return mAlbum; + } + + public Image getImage() { + if (mAlbum.getImage() != null && !TextUtils.isEmpty(mAlbum.getImage().getImagePath())) { + return mAlbum.getImage(); + } else { + return mArtist.getImage(); + } + } + + /** + * @return this {@link Track}'s duration + */ + public long getDuration() { + return mDuration; + } + + /** + * Set this {@link Track}'s duration + */ + public void setDuration(long duration) { + this.mDuration = duration; + } + + /** + * @return this {@link Track}'s track number + */ + public int getAlbumPos() { + return mAlbumPos; + } + + /** + * Set this {@link Track}'s track number + */ + public void setAlbumPos(int albumPos) { + this.mAlbumPos = albumPos; + } + + /** + * @return this {@link Track}'s year + */ + public int getYear() { + return mYear; + } + + /** + * Set this {@link Track}'s year + */ + public void setYear(int year) { + this.mYear = year; + } + + /** + * @return this {@link Track}'s disc number + */ + public int getDiscNumber() { + return mDiscNumber; + } + + /** + * Set this {@link Track}'s disc number + */ + public void setDiscNumber(int discNumber) { + mDiscNumber = discNumber; + } + + public String toShortString() { + return "'" + getName() + "'" + " by " + getArtist().toShortString() + " on " + + getAlbum().toShortString(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( " + toShortString() + " )@" + + Integer.toHexString(hashCode()); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/collection/UserCollection.java b/app/src/main/java/org/tomahawk/libtomahawk/collection/UserCollection.java new file mode 100644 index 000000000..b9763f0f5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/collection/UserCollection.java @@ -0,0 +1,509 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.collection; + +import org.jdeferred.Deferred; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.database.CollectionDb; +import org.tomahawk.libtomahawk.database.CollectionDbManager; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.database.UserCollectionDb; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.UserCollectionStubResolver; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverTrack; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.mediaplayers.VLCMediaPlayer; +import org.tomahawk.tomahawk_android.utils.MediaWrapper; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.utils.WeakReferenceHandler; +import org.videolan.libvlc.Media; +import org.videolan.libvlc.util.AndroidUtil; +import org.videolan.libvlc.util.Extensions; + +import android.net.Uri; +import android.os.Environment; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.widget.ImageView; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * This class represents a user's local {@link UserCollection}. + */ +public class UserCollection extends DbCollection { + + private static final String TAG = UserCollection.class.getSimpleName(); + + private static final String HAS_SET_DEFAULTDIRS + = "org.tomahawk.tomahawk_android.has_set_defaultdirs"; + + private static final List TYPE_WHITELIST = Arrays.asList("vfat", "exfat", "sdcardfs", + "fuse", "ntfs", "fat32", "ext3", "ext4", "esdfs"); + + private static final List TYPE_BLACKLIST = Collections.singletonList("tmpfs"); + + private static final String[] MOUNT_WHITELIST = {"/mnt", "/Removable", "/storage"}; + + private static final String[] MOUNT_BLACKLIST = {"/mnt/secure", "/mnt/shell", "/mnt/asec", + "/mnt/obb", "/mnt/media_rw/extSdCard", "/mnt/media_rw/sdcard", "/storage/emulated"}; + + private static final String[] DEVICE_WHITELIST = {"/dev/block/vold", "/dev/fuse", + "/mnt/media_rw"}; + + public final static HashSet FOLDER_BLACKLIST; + + static { + final String[] folder_blacklist = {"/alarms", "/notifications", "/ringtones", + "/media/alarms", "/media/notifications", "/media/ringtones", "/media/audio/alarms", + "/media/audio/notifications", "/media/audio/ringtones", "/Android/data/"}; + + FOLDER_BLACKLIST = new HashSet<>(); + for (String item : folder_blacklist) { + FOLDER_BLACKLIST + .add(android.os.Environment.getExternalStorageDirectory().getPath() + item); + } + } + + private boolean mIsStopping = false; + + private boolean mRestart = false; + + private Thread mLoadingThread; + + private final ConcurrentHashMap mQueryTimeStamps + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap mArtistTimeStamps + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap mAlbumTimeStamps + = new ConcurrentHashMap<>(); + + public UserCollection() { + super(UserCollectionStubResolver.get()); + } + + @Override + public Promise getCollectionId() { + Deferred d = new ADeferredObject<>(); + return d.resolve(TomahawkApp.PLUGINNAME_USERCOLLECTION); + } + + @Override + public void loadIcon(ImageView imageView, boolean grayOut) { + } + + public void loadMediaItems(boolean fullScan) { + if (fullScan) { + Log.d(TAG, "Executing full scan. Wiping cache..."); + DatabaseHelper.get().removeAllMedias(); + } + if (isWorking()) { + if (fullScan) { + // do a clean restart if a scan is ongoing + mRestart = true; + mIsStopping = true; + } + } else { + loadMediaItems(); + } + } + + private void loadMediaItems() { + if (mLoadingThread == null || mLoadingThread.getState() == Thread.State.TERMINATED) { + mIsStopping = false; + mLoadingThread = new Thread(new GetMediaItemsRunnable()); + mLoadingThread.start(); + } + } + + public void stop() { + mIsStopping = true; + } + + public boolean isWorking() { + return mLoadingThread != null && + mLoadingThread.isAlive() && + mLoadingThread.getState() != Thread.State.TERMINATED && + mLoadingThread.getState() != Thread.State.NEW; + } + + private class GetMediaItemsRunnable implements Runnable { + + @Override + public void run() { + Log.d(TAG, "Scanning for local tracks..."); + long time = System.currentTimeMillis(); + Set setDefaultDirs = PreferenceUtils.getStringSet(HAS_SET_DEFAULTDIRS); + if (setDefaultDirs == null) { + setDefaultDirs = new HashSet<>(); + } + for (String defaultDir : getStorageDirectories()) { + if (!setDefaultDirs.contains(defaultDir)) { + Log.d(TAG, "Default directory added: " + defaultDir); + DatabaseHelper.get().addMediaDir(defaultDir); + setDefaultDirs.add(defaultDir); + } + } + PreferenceUtils.edit().putStringSet(HAS_SET_DEFAULTDIRS, setDefaultDirs).commit(); + + List mediaDirs = DatabaseHelper.get().getMediaDirs(false); + Stack directories = new Stack<>(); + directories.addAll(mediaDirs); + for (File dir : directories) { + Log.d(TAG, "Scanning directory: " + dir); + } + + // get all existing media items + HashMap existingMedias = DatabaseHelper.get().getMedias(); + + // list of all added files + HashSet addedLocations = new HashSet<>(); + + ArrayList mediaToScan = new ArrayList<>(); + try { + long listFilesTimeBefore = System.currentTimeMillis(); + final HashSet directoriesScanned = new HashSet<>(); + // Count total files, and stack them + while (!directories.isEmpty()) { + File dir = directories.pop(); + String dirPath = dir.getAbsolutePath(); + + // Skip some system folders + if (dirPath.startsWith("/proc/") || dirPath.startsWith("/sys/") + || dirPath.startsWith("/dev/")) { + continue; + } + + // Do not scan again if same canonical path + try { + dirPath = dir.getCanonicalPath(); + } catch (IOException e) { + Log.e(TAG, "GetMediaItemsRunnable#run() - " + e.getClass() + ": " + + e.getLocalizedMessage()); + } + if (directoriesScanned.contains(dirPath)) { + continue; + } else { + directoriesScanned.add(dirPath); + } + + // Do no scan media in .nomedia folders + if (new File(dirPath + "/.nomedia").exists()) { + continue; + } + + // Filter the extensions and the folders + try { + File[] f = dir.listFiles(new MediaItemFilter()); + if (f != null) { + for (File file : f) { + if (file.isFile()) { + mediaToScan.add(file); + } else if (file.isDirectory()) { + directories.push(file); + } + } + } + } catch (Exception e) { + // listFiles can fail in OutOfMemoryError, go to the next folder + Log.e(TAG, "GetMediaItemsRunnable#run() - " + e.getClass() + ": " + + e.getLocalizedMessage()); + continue; + } + + if (mIsStopping) { + Log.d(TAG, "Stopping scan"); + return; + } + } + long listFilesTime = System.currentTimeMillis() - listFilesTimeBefore; + int parseCounter = 0; + long parsingTimeBefore = System.currentTimeMillis(); + ArrayList mediaWrappers = new ArrayList<>(); + // Process the stacked items + for (File file : mediaToScan) { + String fileURI = AndroidUtil.FileToUri(file).toString(); + if (existingMedias.containsKey(fileURI)) { + //Log.d(TAG, "File has already been scanned: " + fileURI); + // only add file if it is not already in the list. eg. if a user selects a + // subfolder as well + if (!addedLocations.contains(fileURI)) { + //Log.d(TAG, "File added to processing queue: " + fileURI); + // get existing media item from database + mediaWrappers.add(existingMedias.get(fileURI)); + addedLocations.add(fileURI); + } + } else { + // create new media item + final Media media = new Media(VLCMediaPlayer.getLibVlcInstance(), + Uri.parse(fileURI)); + media.parse(); + parseCounter++; + // skip files with .mod extension and no duration + if ((media.getDuration() == 0 || (media.getTrackCount() != 0 + && TextUtils.isEmpty(media.getTrack(0).codec))) + && fileURI.endsWith(".mod")) { + Log.d(TAG, "File skipped: " + fileURI); + continue; + } + //Log.d(TAG, "File added to database and processing queue: " + fileURI); + MediaWrapper mw = new MediaWrapper(media); + media.release(); + mw.setLastModified(file.lastModified()); + mediaWrappers.add(mw); + } + if (mIsStopping) { + Log.d(TAG, "Stopping scan"); + return; + } + } + Log.d(TAG, "Listing files took " + listFilesTime + "ms."); + Log.d(TAG, "Scanned " + mediaToScan.size() + " files."); + Log.d(TAG, "Actually parsed " + parseCounter + " files."); + Log.d(TAG, + "Parsing took " + (System.currentTimeMillis() - parsingTimeBefore) + "ms."); + // Add all items to the database + DatabaseHelper.get().addMedias(mediaWrappers); + + processMediaWrappers(mediaWrappers); + } finally { + // remove old files & folders from database if storage is mounted + if (!mIsStopping && Environment.getExternalStorageState() + .equals(Environment.MEDIA_MOUNTED)) { + for (String fileURI : addedLocations) { + existingMedias.remove(fileURI); + } + Log.d(TAG, "Removed " + existingMedias.keySet().size() + + " media items from database"); + DatabaseHelper.get().removeMedias(existingMedias.keySet()); + } + + if (mRestart) { + Log.d(TAG, "Restarting scan"); + mRestart = false; + mRestartHandler.sendEmptyMessageDelayed(1, 200); + } + EventBus.getDefault().post(new CollectionManager.UpdatedEvent()); + Log.d(TAG, "Scanning process finished in " + (System.currentTimeMillis() - time) + + "ms"); + } + } + + private void processMediaWrappers(List mws) { + Log.d(TAG, "Processing " + mws.size() + " media items..."); + Map> albumArtistsMap = new HashMap<>(); + for (MediaWrapper mw : mws) { + if (mw.getType() == MediaWrapper.TYPE_AUDIO) { + String albumKey = mw.getAlbum() != null ? mw.getAlbum().toLowerCase() : ""; + if (albumArtistsMap.get(albumKey) == null) { + albumArtistsMap.put(albumKey, new HashSet()); + } + albumArtistsMap.get(albumKey).add(mw.getArtist()); + } + } + List tracks = new ArrayList<>(); + for (MediaWrapper mw : mws) { + if (mw.getType() == MediaWrapper.TYPE_AUDIO) { + ScriptResolverTrack track = new ScriptResolverTrack(); + track.album = mw.getAlbum(); + track.albumArtist = mw.getAlbumArtist(); + track.track = mw.getTitle(); + track.artist = mw.getArtist(); + track.duration = mw.getLength() / 1000; + track.albumpos = mw.getTrackNumber(); + track.url = mw.getLocation(); + track.imagePath = mw.getArtworkURL(); + track.lastModified = mw.getLastModified(); + tracks.add(track); + } + } + CollectionDb db = CollectionDbManager.get().getCollectionDb(getId()); + db.wipe(); + db.addTracks(tracks); + Log.d(TAG, "Processed " + mws.size() + " media items. " + tracks.size() + + " tracks have been added to the UserCollection."); + } + } + + public ConcurrentHashMap getQueryTimeStamps() { + return mQueryTimeStamps; + } + + public ConcurrentHashMap getArtistTimeStamps() { + return mArtistTimeStamps; + } + + public ConcurrentHashMap getAlbumTimeStamps() { + return mAlbumTimeStamps; + } + + private final RestartHandler mRestartHandler = new RestartHandler(this); + + private static class RestartHandler extends WeakReferenceHandler { + + public RestartHandler(UserCollection userCollection) { + super(Looper.getMainLooper(), userCollection); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + getReferencedObject().loadMediaItems(); + } + } + } + + /** + * Filters all irrelevant files + */ + private static class MediaItemFilter implements FileFilter { + + @Override + public boolean accept(File f) { + boolean accepted = false; + if (!f.isHidden()) { + if (f.isDirectory() + && !FOLDER_BLACKLIST.contains(f.getPath().toLowerCase(Locale.ENGLISH))) { + accepted = true; + } else { + String fileName = f.getName().toLowerCase(Locale.ENGLISH); + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex != -1) { + String fileExt = fileName.substring(dotIndex); + accepted = Extensions.AUDIO.contains(fileExt); + } + } + } + return accepted; + } + } + + public static ArrayList getStorageDirectories() { + BufferedReader bufReader = null; + ArrayList list = new ArrayList<>(); + list.add(Environment.getExternalStorageDirectory().getPath()); + + try { + bufReader = new BufferedReader(new FileReader("/proc/mounts")); + String line; + while ((line = bufReader.readLine()) != null) { + + StringTokenizer tokens = new StringTokenizer(line, " "); + String device = tokens.nextToken(); + String mountpoint = tokens.nextToken(); + String type = tokens.nextToken(); + + // skip if already in list or if type/mountpoint is blacklisted + if (list.contains(mountpoint) || TYPE_BLACKLIST.contains(type) + || doStringsStartWith(MOUNT_BLACKLIST, mountpoint)) { + continue; + } + + // check that device is in whitelist, and either type or mountpoint is in a whitelist + if (doStringsStartWith(DEVICE_WHITELIST, device) && (TYPE_WHITELIST.contains(type) + || doStringsStartWith(MOUNT_WHITELIST, mountpoint))) { + list.add(mountpoint); + } + } + } catch (IOException e) { + Log.e(TAG, "getStorageDirectories: " + e.getClass() + ": " + e.getLocalizedMessage()); + } finally { + if (bufReader != null) { + try { + bufReader.close(); + } catch (IOException e) { + Log.e(TAG, "getStorageDirectories: " + e.getClass() + ": " + + e.getLocalizedMessage()); + } + } + } + return list; + } + + private static boolean doStringsStartWith(String[] array, String text) { + for (String item : array) { + if (text.startsWith(item)) { + return true; + } + } + return false; + } + + public void addLovedArtists(List artists, List lastModifieds) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + db.addArtists(artists, lastModifieds); + } + + public void removeLoved(Artist artist) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + db.remove(artist); + } + + + public boolean isLoved(Artist artist) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + return db.isLoved(artist); + } + + + public void addLovedAlbums(List albums, List lastModifieds) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + db.addAlbums(albums, lastModifieds); + } + + public void removeLoved(Album album) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + db.remove(album); + } + + + public boolean isLoved(Album album) { + UserCollectionDb db = (UserCollectionDb) CollectionDbManager.get().getCollectionDb( + TomahawkApp.PLUGINNAME_USERCOLLECTION); + return db.isLoved(album); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDb.java b/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDb.java new file mode 100644 index 000000000..833958f31 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDb.java @@ -0,0 +1,903 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.database; + +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.DbCollection; +import org.tomahawk.libtomahawk.resolver.FuzzyIndex; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverTrack; +import org.tomahawk.libtomahawk.utils.StringUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CollectionDb extends SQLiteOpenHelper { + + public static final String TAG = CollectionDb.class.getSimpleName(); + + public static final String ID = "_id"; + + public static final String TABLE_ARTISTS = "artists"; + + public static final String ARTISTS_ARTIST = "artist"; + + public static final String ARTISTS_ARTISTDISAMBIGUATION = "artistDisambiguation"; + + public static final String ARTISTS_LASTMODIFIED = "artistLastModified"; + + public static final String ARTISTS_TYPE = "artistType"; + + public static final String TABLE_ALBUMARTISTS = "albumArtists"; + + public static final String ALBUMARTISTS_ALBUMARTIST = "albumArtist"; + + public static final String ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION = "albumArtistDisambiguation"; + + public static final String ALBUMARTISTS_LASTMODIFIED = "albumArtistLastModified"; + + public static final String TABLE_ALBUMS = "albums"; + + public static final String ALBUMS_ALBUM = "album"; + + public static final String ALBUMS_IMAGEPATH = "imagePath"; + + public static final String ALBUMS_ALBUMARTISTID = "albumArtistId"; + + public static final String ALBUMS_LASTMODIFIED = "albumLastModified"; + + public static final String ALBUMS_TYPE = "albumType"; + + public static final String TABLE_ARTISTALBUMS = "artistAlbums"; + + public static final String ARTISTALBUMS_ALBUMID = "albumId"; + + public static final String ARTISTALBUMS_ARTISTID = "artistId"; + + public static final String TABLE_TRACKS = "tracks"; + + public static final String TRACKS_TRACK = "track"; + + public static final String TRACKS_ARTISTID = "artistId"; + + public static final String TRACKS_ALBUMID = "albumId"; + + public static final String TRACKS_URL = "url"; + + public static final String TRACKS_DURATION = "duration"; + + public static final String TRACKS_ALBUMPOS = "albumPos"; + + public static final String TRACKS_LINKURL = "linkUrl"; + + public static final String TRACKS_LASTMODIFIED = "trackLastModified"; + + public static final String TABLE_REVISIONHISTORY = "revisionHistory"; + + public static final String REVISIONHISTORY_ACTION = "action"; + + public static final String REVISIONHISTORY_TRACKCOUNT = "trackCount"; + + public static final String REVISIONHISTORY_REVISION = "revision"; + + public static final String REVISIONHISTORY_TIMESTAMP = "timeStamp"; + + protected static final int ACTION_WIPE = 0; + + protected static final int ACTION_ADDTRACKS = 1; + + protected static final int TYPE_DEFAULT = 0; + + // This type marks an entry that has been explicitly loved. + // E.g. an artist that has been loved by the user. + protected static final int TYPE_HATCHET_EXPLICIT = 1; + + // This type marks an entry that has not been explicitly loved by the user but has been added to + // the collection because it was necessary in order to love another entry. E.g. an artist that + // was added implicitly because an album has been loved by the user. + protected static final int TYPE_HATCHET_IMPLICIT = 2; + + private static final String CREATE_TABLE_ARTISTS = "CREATE TABLE IF NOT EXISTS " + + TABLE_ARTISTS + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ARTISTS_ARTIST + " TEXT," + + ARTISTS_ARTISTDISAMBIGUATION + " TEXT," + + ARTISTS_LASTMODIFIED + " INTEGER," + + ARTISTS_TYPE + " INTEGER," + + "UNIQUE (" + ARTISTS_ARTIST + ", " + ARTISTS_ARTISTDISAMBIGUATION + ", " + + ARTISTS_TYPE + + ") ON CONFLICT IGNORE);"; + + private static final String CREATE_TABLE_ALBUMARTISTS = "CREATE TABLE IF NOT EXISTS " + + TABLE_ALBUMARTISTS + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ALBUMARTISTS_ALBUMARTIST + " TEXT," + + ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION + " TEXT," + + ALBUMARTISTS_LASTMODIFIED + " INTEGER," + + "UNIQUE (" + ALBUMARTISTS_ALBUMARTIST + ", " + ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION + + ") ON CONFLICT IGNORE);"; + + private static final String CREATE_TABLE_ALBUMS = "CREATE TABLE IF NOT EXISTS " + + TABLE_ALBUMS + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ALBUMS_ALBUM + " TEXT," + + ALBUMS_ALBUMARTISTID + " INTEGER," + + ALBUMS_IMAGEPATH + " TEXT," + + ALBUMS_LASTMODIFIED + " INTEGER," + + ALBUMS_TYPE + " INTEGER," + + "UNIQUE (" + ALBUMS_ALBUM + ", " + ALBUMS_ALBUMARTISTID + ", " + ALBUMS_TYPE + + ") ON CONFLICT IGNORE," + + "FOREIGN KEY(" + ALBUMS_ALBUMARTISTID + ") REFERENCES " + + TABLE_ALBUMARTISTS + "(" + ID + "));"; + + private static final String CREATE_TABLE_ARTISTALBUMS = "CREATE TABLE IF NOT EXISTS " + + TABLE_ARTISTALBUMS + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ARTISTALBUMS_ALBUMID + " INTEGER," + + ARTISTALBUMS_ARTISTID + " INTEGER," + + "UNIQUE (" + ARTISTALBUMS_ALBUMID + ", " + ARTISTALBUMS_ARTISTID + + ") ON CONFLICT IGNORE," + + "FOREIGN KEY(" + ARTISTALBUMS_ALBUMID + ") REFERENCES " + + TABLE_ALBUMS + "(" + ID + ")," + + "FOREIGN KEY(" + ARTISTALBUMS_ARTISTID + ") REFERENCES " + + TABLE_ARTISTS + "(" + ID + "));"; + + private static final String CREATE_TABLE_TRACKS = "CREATE TABLE IF NOT EXISTS " + + TABLE_TRACKS + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + TRACKS_TRACK + " TEXT," + + TRACKS_ARTISTID + " INTEGER," + + TRACKS_ALBUMID + " INTEGER," + + TRACKS_URL + " TEXT," + + TRACKS_DURATION + " INTEGER," + + TRACKS_ALBUMPOS + " INTEGER," + + TRACKS_LINKURL + " TEXT," + + TRACKS_LASTMODIFIED + " INTEGER," + + "UNIQUE (" + TRACKS_TRACK + ", " + TRACKS_ARTISTID + ", " + TRACKS_ALBUMID + + ") ON CONFLICT IGNORE," + + "FOREIGN KEY(" + TRACKS_ARTISTID + ") REFERENCES " + + TABLE_ARTISTS + "(" + ID + ")," + + "FOREIGN KEY(" + TRACKS_ALBUMID + ") REFERENCES " + + TABLE_ALBUMS + "(" + ID + "));"; + + private static final String CREATE_TABLE_REVISIONHISTORY = "CREATE TABLE IF NOT EXISTS " + + TABLE_REVISIONHISTORY + " (" + + ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + REVISIONHISTORY_ACTION + " INTEGER," + + REVISIONHISTORY_TRACKCOUNT + " INTEGER," + + REVISIONHISTORY_REVISION + " TEXT," + + REVISIONHISTORY_TIMESTAMP + " INTEGER );"; + + private static final int DB_VERSION = 5; + + private static final String DB_FILE_SUFFIX = "_collection.db"; + + protected final SQLiteDatabase mDb; + + private static final String LAST_COLLECTION_DB_UPDATE_SUFFIX = "_last_collection_db_update"; + + public static class WhereInfo { + + public String connection; + + public Map where = new HashMap<>(); + + public boolean equals = true; + + } + + private static class JoinInfo { + + String table; + + Map conditions = new HashMap<>(); + + } + + private String mCollectionId; + + private FuzzyIndex mFuzzyIndex; + + public CollectionDb(Context context, String collectionId) { + super(context, collectionId + DB_FILE_SUFFIX, null, DB_VERSION); + + Log.d(TAG, "Constructed CollectionDb '" + collectionId + DB_FILE_SUFFIX + "' with version " + + DB_VERSION + ", objectId: " + this.hashCode()); + + mCollectionId = collectionId; + + close(); + mDb = getWritableDatabase(); + + mFuzzyIndex = new FuzzyIndex(this); + } + + public String getCollectionId() { + return mCollectionId; + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.d(TAG, "onCreate - CollectionDb '" + db.getPath() + "' with version " + + db.getVersion() + ", objectId: " + this.hashCode()); + db.execSQL(CREATE_TABLE_ARTISTS); + db.execSQL(CREATE_TABLE_ALBUMARTISTS); + db.execSQL(CREATE_TABLE_ALBUMS); + db.execSQL(CREATE_TABLE_ARTISTALBUMS); + db.execSQL(CREATE_TABLE_TRACKS); + db.execSQL(CREATE_TABLE_REVISIONHISTORY); + Log.d(TAG, "onCreate finished - CollectionDb '" + db.getPath() + "' with version " + + db.getVersion() + ", objectId: " + this.hashCode()); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + + ", which might destroy all old data"); + if (oldVersion < 4) { + wipe(db); + } + if (oldVersion < 5) { + db.execSQL(CREATE_TABLE_REVISIONHISTORY); + long lastDbUpdate = + PreferenceUtils.getLong(mCollectionId + LAST_COLLECTION_DB_UPDATE_SUFFIX); + if (lastDbUpdate > 0) { + storeNewRevision(db, String.valueOf(lastDbUpdate), ACTION_ADDTRACKS); + } + } + } + + public synchronized void addTracks(List tracks) { + long time = System.currentTimeMillis(); + + // Check if we want to store the album as a compilation album (with artist "Various Artists") + Map> albumArtists = new HashMap<>(); + for (ScriptResolverTrack track : tracks) { + if (track.artist == null) { + track.artist = ""; + } + if (track.artistDisambiguation == null) { + track.artistDisambiguation = ""; + } + if (track.album == null) { + track.album = ""; + } + if (track.albumArtist == null) { + track.albumArtist = ""; + } + if (track.albumArtistDisambiguation == null) { + track.albumArtistDisambiguation = ""; + } + if (track.track == null) { + track.track = ""; + } + Set artists = albumArtists.get(track.album + "♠" + track.albumArtist); + if (artists == null) { + artists = new HashSet<>(); + albumArtists.put(track.album + "♠" + track.albumArtist, artists); + } + if (artists.size() < 2) { + artists.add(track.artist); + } + } + + Map artistLastModifiedMap = new HashMap<>(); + // First we insert all artists and albumArtists + mDb.beginTransaction(); + for (ScriptResolverTrack track : tracks) { + if (albumArtists.get(track.album + "♠" + track.albumArtist).size() > 1) { + ContentValues values = new ContentValues(); + values.put(ARTISTS_ARTIST, Artist.COMPILATION_ARTIST.getName()); + values.put(ARTISTS_ARTISTDISAMBIGUATION, ""); + String artistKey = Artist.COMPILATION_ARTIST.getName() + "♠" + ""; + Long lastModified = artistLastModifiedMap.get(artistKey); + if (lastModified == null || lastModified < track.lastModified) { + artistLastModifiedMap.put(artistKey, track.lastModified); + lastModified = track.lastModified; + } + values.put(ARTISTS_LASTMODIFIED, lastModified); + values.put(ARTISTS_TYPE, TYPE_DEFAULT); + mDb.insert(TABLE_ARTISTS, null, values); + } + ContentValues values = new ContentValues(); + values.put(ARTISTS_ARTIST, track.artist); + values.put(ARTISTS_ARTISTDISAMBIGUATION, track.artistDisambiguation); + String artistKey = track.artist + "♠" + track.artistDisambiguation; + Long lastModified = artistLastModifiedMap.get(artistKey); + if (lastModified == null || lastModified < track.lastModified) { + artistLastModifiedMap.put(artistKey, track.lastModified); + lastModified = track.lastModified; + } + values.put(ARTISTS_LASTMODIFIED, lastModified); + values.put(ARTISTS_TYPE, TYPE_DEFAULT); + mDb.insert(TABLE_ARTISTS, null, values); + values = new ContentValues(); + values.put(ALBUMARTISTS_ALBUMARTIST, track.albumArtist); + values.put(ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION, track.albumArtistDisambiguation); + values.put(ALBUMARTISTS_LASTMODIFIED, lastModified); + mDb.insert(TABLE_ALBUMARTISTS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + + Cursor cursor = mDb.query(TABLE_ARTISTS, + new String[]{ID, ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION}, + null, null, null, null, null); + Map cachedArtists = cursorToMap(cursor); + + Map albumLastModifiedMap = new HashMap<>(); + mDb.beginTransaction(); + for (ScriptResolverTrack track : tracks) { + ContentValues values = new ContentValues(); + values.put(ALBUMS_ALBUM, track.album); + int albumArtistId; + if (albumArtists.get(track.album + "♠" + track.albumArtist).size() == 1) { + albumArtistId = cachedArtists.get( + concatKeys(track.artist, track.artistDisambiguation)); + } else { + albumArtistId = cachedArtists.get( + concatKeys(Artist.COMPILATION_ARTIST.getName(), "")); + } + values.put(ALBUMS_ALBUMARTISTID, albumArtistId); + values.put(ALBUMS_IMAGEPATH, track.imagePath); + String artistKey = track.album + "♠" + albumArtistId; + Long lastModified = albumLastModifiedMap.get(artistKey); + if (lastModified == null || lastModified < track.lastModified) { + albumLastModifiedMap.put(artistKey, track.lastModified); + lastModified = track.lastModified; + } + values.put(ALBUMS_LASTMODIFIED, lastModified); + values.put(ALBUMS_TYPE, TYPE_DEFAULT); + mDb.insert(TABLE_ALBUMS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + + cursor = mDb.query(TABLE_ALBUMS, + new String[]{ID, ALBUMS_ALBUM, ALBUMS_ALBUMARTISTID}, + null, null, null, null, null); + Map cachedAlbums = cursorToMap(cursor); + + mDb.beginTransaction(); + for (ScriptResolverTrack track : tracks) { + ContentValues values = new ContentValues(); + int albumArtistId; + if (albumArtists.get(track.album + "♠" + track.albumArtist).size() == 1) { + albumArtistId = cachedArtists.get( + concatKeys(track.artist, track.artistDisambiguation)); + } else { + albumArtistId = cachedArtists.get( + concatKeys(Artist.COMPILATION_ARTIST.getName(), "")); + } + int artistId = cachedArtists.get(concatKeys(track.artist, track.artistDisambiguation)); + int albumId = cachedAlbums.get(concatKeys(track.album, albumArtistId)); + values.put(ARTISTALBUMS_ARTISTID, artistId); + values.put(ARTISTALBUMS_ALBUMID, albumId); + mDb.insert(TABLE_ARTISTALBUMS, null, values); + values = new ContentValues(); + values.put(TRACKS_TRACK, track.track); + values.put(TRACKS_ARTISTID, artistId); + values.put(TRACKS_ALBUMID, albumId); + values.put(TRACKS_URL, track.url); + values.put(TRACKS_DURATION, (int) track.duration); + values.put(TRACKS_LINKURL, track.linkUrl); + values.put(TRACKS_ALBUMPOS, track.albumpos); + values.put(TRACKS_LASTMODIFIED, track.lastModified); + mDb.insert(TABLE_TRACKS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + + Log.d(TAG, "Added " + tracks.size() + " tracks in " + (System.currentTimeMillis() - time) + + "ms"); + if (tracks.size() > 0) { + storeNewRevision(String.valueOf(System.currentTimeMillis()), ACTION_ADDTRACKS); + } + mFuzzyIndex.ensureIndex(); + ((DbCollection) CollectionManager.get().getCollection(mCollectionId)).setInitialized(true); + } + + private static Map cursorToMap(Cursor cursor) { + Map map = new HashMap<>(); + try { + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + do { + map.put(concatKeys(cursor.getString(1), cursor.getString(2)), + cursor.getInt(0)); + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return map; + } + + public synchronized void wipe() { + wipe(mDb); + } + + private void wipe(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_ARTISTS + "`;"); + db.execSQL(CREATE_TABLE_ARTISTS); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_ALBUMARTISTS + "`;"); + db.execSQL(CREATE_TABLE_ALBUMARTISTS); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_ALBUMS + "`;"); + db.execSQL(CREATE_TABLE_ALBUMS); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_ARTISTALBUMS + "`;"); + db.execSQL(CREATE_TABLE_ARTISTALBUMS); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_TRACKS + "`;"); + db.execSQL(CREATE_TABLE_TRACKS); + storeNewRevision(db, String.valueOf(System.currentTimeMillis()), ACTION_WIPE); + } + + /** + * Convenience method. Uses a default set of fields. + */ + public synchronized Cursor tracks(WhereInfo where, String[] orderBy) { + String[] fields = new String[]{ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION, ALBUMS_ALBUM, + TRACKS_TRACK, TRACKS_DURATION, TRACKS_URL, TRACKS_LINKURL, TRACKS_ALBUMPOS, + TRACKS_LASTMODIFIED, TRACKS_ALBUMID}; + return tracks(where, orderBy, fields); + } + + public synchronized Cursor tracks(WhereInfo where, String[] orderBy, String[] fields) { + List joinInfos = new ArrayList<>(); + JoinInfo joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ARTISTS; + joinInfo.conditions.put(TABLE_TRACKS + "." + TRACKS_ARTISTID, TABLE_ARTISTS + "." + ID); + joinInfos.add(joinInfo); + joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ALBUMS; + joinInfo.conditions.put(TABLE_TRACKS + "." + TRACKS_ALBUMID, TABLE_ALBUMS + "." + ID); + joinInfos.add(joinInfo); + String[] groupBy = new String[]{TRACKS_TRACK, ARTISTS_ARTIST, ALBUMS_ALBUM}; + return sqlSelect(TABLE_TRACKS, fields, where, joinInfos, orderBy, groupBy, null, + TRACKS_LASTMODIFIED, false); + } + + public synchronized long tracksCurrentRevision() { + String[] fields = new String[]{TRACKS_LASTMODIFIED}; + long currentRevision = -1; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_TRACKS, fields, null, null, + new String[]{TRACKS_LASTMODIFIED + " DESC"}, null, null, null, false); + if (cursor.moveToFirst()) { + currentRevision = cursor.getLong(0); + } else { + Log.e(TAG, "tracksCurrentRevision - no tracks in table!"); + return -1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return currentRevision; + } + + public synchronized Cursor albums(String[] orderBy) { + String[] fields = new String[]{ALBUMS_ALBUM, ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION, + ALBUMS_IMAGEPATH, ALBUMS_LASTMODIFIED}; + List joinInfos = new ArrayList<>(); + JoinInfo joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ARTISTS; + joinInfo.conditions.put( + TABLE_ALBUMS + "." + ALBUMS_ALBUMARTISTID, TABLE_ARTISTS + "." + ID); + joinInfos.add(joinInfo); + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ALBUMS_ALBUM, new String[]{""}); + whereInfo.equals = false; + String[] groupBy = new String[]{ALBUMS_ALBUM, ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION}; + return sqlSelect(TABLE_ALBUMS, fields, whereInfo, joinInfos, orderBy, groupBy, ALBUMS_TYPE, + ALBUMS_LASTMODIFIED, false); + } + + public synchronized Cursor artists(String[] orderBy) { + String[] fields = new String[]{ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION, + ARTISTS_LASTMODIFIED}; + JoinInfo joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ARTISTS; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{Artist.COMPILATION_ARTIST.getName(), ""}); + whereInfo.equals = false; + String[] groupBy = new String[]{ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION}; + return sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, orderBy, groupBy, ARTISTS_TYPE, + ARTISTS_LASTMODIFIED, false); + } + + public synchronized Cursor albumArtists(String[] orderBy) { + String[] fields = new String[]{ALBUMARTISTS_ALBUMARTIST, + ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION, ALBUMARTISTS_LASTMODIFIED}; + String[] groupBy = new String[]{ALBUMARTISTS_ALBUMARTIST, + ALBUMARTISTS_ALBUMARTISTDISAMBIGUATION}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{Artist.COMPILATION_ARTIST.getName(), ""}); + whereInfo.equals = false; + return sqlSelect(TABLE_ALBUMARTISTS, fields, whereInfo, null, orderBy, groupBy, null, + ALBUMARTISTS_LASTMODIFIED, false); + } + + public synchronized long artistCurrentRevision(String artist, String artistDisambiguation) { + String[] fields = new String[]{ARTISTS_LASTMODIFIED}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{artist}); + whereInfo.where.put(ARTISTS_ARTISTDISAMBIGUATION, new String[]{artistDisambiguation}); + long currentRevision = -1; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, null, null, null, null, + false); + if (cursor.moveToFirst()) { + currentRevision = cursor.getLong(0); + } else { + Log.e(TAG, "artistCurrentRevision - Couldn't find artist with given name!"); + return -1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return currentRevision; + } + + public synchronized Cursor artistAlbums(String artist, String artistDisambiguation) { + String[] fields = new String[]{ID}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{artist}); + whereInfo.where.put(ARTISTS_ARTISTDISAMBIGUATION, new String[]{artistDisambiguation}); + int artistId; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, null, null, ARTISTS_TYPE, + null, true); + if (cursor.moveToFirst()) { + artistId = cursor.getInt(0); + } else { + Log.e(TAG, "artistAlbums - Couldn't find artist with given name!"); + return null; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + fields = new String[]{ALBUMS_ALBUM, ARTISTS_ARTIST, ARTISTS_ARTISTDISAMBIGUATION, + ALBUMS_IMAGEPATH, ALBUMS_LASTMODIFIED}; + whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTALBUMS_ARTISTID, new String[]{String.valueOf(artistId)}); + List joinInfos = new ArrayList<>(); + JoinInfo joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ALBUMS; + joinInfo.conditions.put( + TABLE_ARTISTALBUMS + "." + ARTISTALBUMS_ALBUMID, TABLE_ALBUMS + "." + ID); + joinInfos.add(joinInfo); + joinInfo = new JoinInfo(); + joinInfo.table = TABLE_ARTISTS; + joinInfo.conditions.put( + TABLE_ALBUMS + "." + ALBUMS_ALBUMARTISTID, TABLE_ARTISTS + "." + ID); + joinInfos.add(joinInfo); + return sqlSelect(TABLE_ARTISTALBUMS, fields, whereInfo, joinInfos, + new String[]{ALBUMS_ALBUM}, null, ALBUMS_TYPE, null, true); + } + + public synchronized long albumCurrentRevision(String album, String albumArtist, + String albumArtistDisambiguation) { + String[] fields = new String[]{ID}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{albumArtist}); + whereInfo.where.put(ARTISTS_ARTISTDISAMBIGUATION, new String[]{albumArtistDisambiguation}); + int artistId; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, null, null, null, null, + false); + if (cursor.moveToFirst()) { + artistId = cursor.getInt(0); + } else { + Log.e(TAG, "albumCurrentRevision - Couldn't find artist with given name!"); + return -1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + fields = new String[]{ALBUMS_LASTMODIFIED}; + whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ALBUMS_ALBUM, new String[]{album}); + whereInfo.where.put(ALBUMS_ALBUMARTISTID, new String[]{String.valueOf(artistId)}); + long currentRevision = -1; + cursor = null; + try { + cursor = sqlSelect(TABLE_ALBUMS, fields, whereInfo, null, null, null, null, null, + false); + if (cursor.moveToFirst()) { + currentRevision = cursor.getLong(0); + } else { + Log.e(TAG, "albumCurrentRevision - Couldn't find album with given name!"); + return -1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return currentRevision; + } + + public synchronized Cursor albumTracks(String album, String albumArtist, + String albumArtistDisambiguation) { + String[] fields = new String[]{ID}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{albumArtist}); + whereInfo.where.put(ARTISTS_ARTISTDISAMBIGUATION, new String[]{albumArtistDisambiguation}); + int artistId; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, null, null, ARTISTS_TYPE, + null, true); + if (cursor.moveToFirst()) { + artistId = cursor.getInt(0); + } else { + Log.e(TAG, "albumTracks - Couldn't find artist with given name!"); + return null; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + fields = new String[]{ID}; + whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ALBUMS_ALBUM, new String[]{album}); + whereInfo.where.put(ALBUMS_ALBUMARTISTID, new String[]{String.valueOf(artistId)}); + int albumId; + cursor = null; + try { + cursor = sqlSelect(TABLE_ALBUMS, fields, whereInfo, null, null, null, ALBUMS_TYPE, + null, true); + if (cursor.moveToFirst()) { + albumId = cursor.getInt(0); + } else { + Log.e(TAG, "albumTracks - Couldn't find album with given name!"); + return null; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(TRACKS_ALBUMID, new String[]{String.valueOf(albumId)}); + return tracks(whereInfo, new String[]{TRACKS_ALBUMPOS}); + } + + public synchronized Cursor artistTracks(String artist, String artistDisambiguation) { + String[] fields = new String[]{ID}; + WhereInfo whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(ARTISTS_ARTIST, new String[]{artist}); + whereInfo.where.put(ARTISTS_ARTISTDISAMBIGUATION, new String[]{artistDisambiguation}); + int artistId; + Cursor cursor = null; + try { + cursor = sqlSelect(TABLE_ARTISTS, fields, whereInfo, null, null, null, ARTISTS_TYPE, + null, true); + if (cursor.moveToFirst()) { + artistId = cursor.getInt(0); + } else { + Log.e(TAG, "artistTracks - Couldn't find artist with given name!"); + return null; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + whereInfo = new WhereInfo(); + whereInfo.connection = "AND"; + whereInfo.where.put(TRACKS_ARTISTID, new String[]{String.valueOf(artistId)}); + return tracks(whereInfo, new String[]{TRACKS_ALBUMID}); + } + + private Cursor sqlSelect(String table, String[] fields, WhereInfo where, + List joinInfos, String[] orderBy, String[] groupBy, String typeColumn, + String lastModifiedColumn, boolean filterAllLoved) { + String whereString = ""; + List allWhereValues = new ArrayList<>(); + if (where != null) { + whereString = " WHERE "; + boolean notFirst = false; + for (String whereKey : where.where.keySet()) { + String[] whereValues = where.where.get(whereKey); + for (String whereValue : whereValues) { + if (notFirst) { + whereString += " " + where.connection + " "; + } + notFirst = true; + whereString += table + "." + whereKey + (where.equals ? " = " : " != ") + "?"; + allWhereValues.add(whereValue); + } + } + } + if (typeColumn != null) { + if (whereString.isEmpty()) { + whereString = " WHERE "; + } else { + whereString += " AND "; + } + // filter out all implicitly added items + whereString += typeColumn + " != ?"; + allWhereValues.add(String.valueOf(TYPE_HATCHET_IMPLICIT)); + } + if (filterAllLoved) { + if (whereString.isEmpty()) { + whereString = " WHERE "; + } else { + whereString += " AND "; + } + // filter out all explicitly added items + whereString += typeColumn + " != ?"; + allWhereValues.add(String.valueOf(TYPE_HATCHET_EXPLICIT)); + } + + String joinString = ""; + if (joinInfos != null) { + for (JoinInfo joinInfo : joinInfos) { + joinString += " INNER JOIN " + joinInfo.table + " ON "; + boolean notFirst = false; + for (String joinKey : joinInfo.conditions.keySet()) { + if (notFirst) { + joinString += " AND "; + } + notFirst = true; + joinString += joinKey + " = " + joinInfo.conditions.get(joinKey); + } + } + } + + String orderString = StringUtils.join(" , ", orderBy); + if (orderBy != null) { + orderString = " ORDER BY " + orderString; + } else { + orderString = ""; + } + + String deduplicationOrderString = ""; + String groupString = StringUtils.join(" , ", groupBy); + if (groupBy != null) { + groupString = " GROUP BY " + groupString; + if (lastModifiedColumn != null) { + deduplicationOrderString = " ORDER BY " + lastModifiedColumn; + } + } else { + groupString = ""; + } + + String fieldsString = StringUtils.join(", ", fields); + if (fields == null) { + fieldsString = "*"; + } + + String statement = "SELECT * FROM ( SELECT " + fieldsString + " FROM " + table + joinString + + whereString + deduplicationOrderString + " ) " + groupString + orderString; + String[] allWhereValuesArray = null; + if (allWhereValues.size() > 0) { + allWhereValuesArray = allWhereValues.toArray(new String[allWhereValues.size()]); + } + return mDb.rawQuery(statement, allWhereValuesArray); + } + + private void storeNewRevision(String revision, int action) { + storeNewRevision(mDb, revision, action); + } + + private static void storeNewRevision(SQLiteDatabase db, String revision, int action) { + Cursor cursor = db.query(TABLE_TRACKS, new String[]{ID}, null, null, null, null, null); + int trackCount = cursor.getCount(); + cursor.close(); + + db.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(REVISIONHISTORY_ACTION, action); + values.put(REVISIONHISTORY_TRACKCOUNT, trackCount); + values.put(REVISIONHISTORY_REVISION, revision); + values.put(REVISIONHISTORY_TIMESTAMP, System.currentTimeMillis()); + db.insert(TABLE_REVISIONHISTORY, null, values); + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public String getRevision() { + Cursor cursor = null; + try { + cursor = mDb.query(TABLE_REVISIONHISTORY, new String[]{REVISIONHISTORY_REVISION}, + null, null, null, null, REVISIONHISTORY_TIMESTAMP + " DESC", "1"); + if (!cursor.moveToFirst()) { + return null; + } + return cursor.getString(0); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public long getLastUpdated() { + Cursor cursor = null; + try { + cursor = mDb.query(TABLE_REVISIONHISTORY, new String[]{REVISIONHISTORY_TIMESTAMP}, + null, null, null, null, REVISIONHISTORY_TIMESTAMP + " DESC", "1"); + if (!cursor.moveToFirst()) { + return -1; + } + return cursor.getLong(0); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private static String concatKeys(Object... keys) { + String result = ""; + for (int i = 0; i < keys.length; i++) { + if (i > 0) { + result += "♣"; + } + result += keys[i]; + } + return result; + } + + public FuzzyIndex getFuzzyIndex() { + return mFuzzyIndex; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDbManager.java b/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDbManager.java new file mode 100644 index 000000000..ccbf89f79 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/database/CollectionDbManager.java @@ -0,0 +1,57 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.database; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import java.util.HashMap; +import java.util.Map; + +public class CollectionDbManager { + + public static final String TAG = CollectionDbManager.class.getSimpleName(); + + private static class Holder { + + private static final CollectionDbManager instance = new CollectionDbManager(); + + } + + private Map mCollectionDbs = new HashMap<>(); + + private CollectionDbManager() { + } + + public static CollectionDbManager get() { + return Holder.instance; + } + + public synchronized CollectionDb getCollectionDb(String collectionId) { + CollectionDb db = mCollectionDbs.get(collectionId); + if (db == null) { + if (collectionId.equals(TomahawkApp.PLUGINNAME_USERCOLLECTION)) { + db = new UserCollectionDb(TomahawkApp.getContext(), collectionId); + } else { + db = new CollectionDb(TomahawkApp.getContext(), collectionId); + } + mCollectionDbs.put(collectionId, db); + } + return db; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/database/DatabaseHelper.java b/app/src/main/java/org/tomahawk/libtomahawk/database/DatabaseHelper.java new file mode 100644 index 000000000..28b191c2a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/database/DatabaseHelper.java @@ -0,0 +1,1176 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.database; + +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistComparator; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.QueryParams; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.MediaWrapper; +import org.videolan.libvlc.util.AndroidUtil; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import de.greenrobot.event.EventBus; + +/** + * This class provides a way of storing user created {@link org.tomahawk.libtomahawk.collection.Playlist}s + * in the database + */ +public class DatabaseHelper { + + private static final String TAG = DatabaseHelper.class.getSimpleName(); + + private static final String LOVEDITEMS_PLAYLIST_ID = "loveditems_playlist_id"; + + public static final int FALSE = 0; + + public static final int TRUE = 1; + + public static final int CHUNK_SIZE = 50; + + private static class Holder { + + private static final DatabaseHelper instance = new DatabaseHelper(); + + } + + public static class PlaylistsUpdatedEvent { + + public String mPlaylistId; + } + + // Database fields + private final SQLiteDatabase mDatabase; + + private DatabaseHelper() { + TomahawkSQLiteHelper dbHelper = new TomahawkSQLiteHelper(TomahawkApp.getContext()); + dbHelper.close(); + mDatabase = dbHelper.getWritableDatabase(); + } + + public static DatabaseHelper get() { + return Holder.instance; + } + + /** + * Store the given {@link Playlist} + * + * @param playlist the given {@link Playlist} + * @param reverseEntries set to true, if the order of the entries should be reversed before + * storing in the database + */ + public void storePlaylist(Playlist playlist, boolean reverseEntries) { + storePlaylist(playlist.getId(), playlist, reverseEntries); + } + + /** + * Store the given {@link Playlist} + * + * @param playlist the given {@link Playlist} + * @param reverseEntries set to true, if the order of the entries should be reversed before + * storing in the database + */ + public void storeLovedItemsPlaylist(Playlist playlist, boolean reverseEntries) { + storePlaylist(LOVEDITEMS_PLAYLIST_ID, playlist, reverseEntries); + } + + /** + * Store the given {@link Playlist} + * + * @param playlistId the id under which to store the given {@link Playlist} + * @param playlist the given {@link Playlist} + * @param reverseEntries set to true, if the order of the entries should be reversed before + * storing in the database + */ + private void storePlaylist(final String playlistId, final Playlist playlist, + final boolean reverseEntries) { + List entries = playlist.getEntries(); + + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, playlist.getName()); + if (playlist.isFilled()) { + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + playlist.getCurrentRevision()); + } + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID, playlistId); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, + playlist.getHatchetId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT, entries.size()); + + mDatabase.beginTransaction(); + mDatabase.insertWithOnConflict(TomahawkSQLiteHelper.TABLE_PLAYLISTS, null, + values, + SQLiteDatabase.CONFLICT_REPLACE); + // Delete every already associated Track entry + mDatabase.delete(TomahawkSQLiteHelper.TABLE_TRACKS, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ?", + new String[]{playlistId}); + + // Store every single Track in the database and store the relationship + // by storing the playlists's id with it + for (int i = 0; i < entries.size(); i++) { + PlaylistEntry entry; + if (reverseEntries) { + entry = entries.get(entries.size() - 1 - i); + } else { + entry = entries.get(i); + } + values.clear(); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID, playlistId); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME, + entry.getQuery().getBasicTrack().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME, + entry.getQuery().getBasicTrack().getArtist().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ALBUMNAME, + entry.getQuery().getBasicTrack().getAlbum().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_RESULTHINT, + entry.getQuery().getTopTrackResultKey()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYINDEX, i); + if (entry.getQuery().isFetchedViaHatchet()) { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + TRUE); + } else { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + FALSE); + } + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYID, + entry.getId()); + mDatabase.insert(TomahawkSQLiteHelper.TABLE_TRACKS, null, values); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlistId; + EventBus.getDefault().post(event); + } + + /** + * Rename the given {@link Playlist} + * + * @param playlist the given {@link Playlist} + * @param newName the new playlist name + */ + public void renamePlaylist(final Playlist playlist, final String newName) { + if (playlist != null) { + String topArtistsString = ""; + for (String s : playlist.getTopArtistNames()) { + topArtistsString += s + "\t\t"; + } + + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, newName); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID, playlist.getId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, playlist.getName()); + if (playlist.isFilled()) { + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + playlist.getCurrentRevision()); + } + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, + playlist.getHatchetId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TOPARTISTS, + topArtistsString); + + mDatabase.beginTransaction(); + mDatabase.insertWithOnConflict(TomahawkSQLiteHelper.TABLE_PLAYLISTS, null, + values, SQLiteDatabase.CONFLICT_REPLACE); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlist.getId(); + EventBus.getDefault().post(event); + } else { + Log.e(TAG, "renamePlaylist: playlist is null"); + } + } + + /** + * Update the given {@link Playlist} + * + * @param playlist the given {@link Playlist} + */ + public void updatePlaylist(final Playlist playlist) { + if (playlist != null) { + String topArtistsString = ""; + for (String s : playlist.getTopArtistNames()) { + topArtistsString += s + "\t\t"; + } + + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID, playlist.getId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, playlist.getName()); + if (playlist.isFilled()) { + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + playlist.getCurrentRevision()); + } + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, + playlist.getHatchetId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TOPARTISTS, + topArtistsString); + + mDatabase.beginTransaction(); + mDatabase.insertWithOnConflict(TomahawkSQLiteHelper.TABLE_PLAYLISTS, null, + values, SQLiteDatabase.CONFLICT_REPLACE); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlist.getId(); + EventBus.getDefault().post(event); + } else { + Log.e(TAG, "updatePlaylist: playlist is null"); + } + } + + /** + * Update the given Playlist's hatchet id + * + * @param playlistId the id of the playlist to update + * @param hatchetId the new hatchet id to set + */ + public void updatePlaylistHatchetId(final String playlistId, + final String hatchetId) { + Playlist playlist = getEmptyPlaylist(playlistId); + if (playlist != null) { + String topArtistsString = ""; + if (playlist.getTopArtistNames() != null) { + for (String s : playlist.getTopArtistNames()) { + topArtistsString += s + "\t\t"; + } + } + + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID, playlist.getId()); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, playlist.getName()); + if (playlist.isFilled()) { + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + playlist.getCurrentRevision()); + } + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, hatchetId); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TOPARTISTS, + topArtistsString); + + mDatabase.beginTransaction(); + mDatabase.insertWithOnConflict(TomahawkSQLiteHelper.TABLE_PLAYLISTS, null, + values, SQLiteDatabase.CONFLICT_REPLACE); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlist.getId(); + EventBus.getDefault().post(event); + } else { + Log.e(TAG, "updatePlaylistHatchetId: playlist is null, id: " + playlistId); + } + } + + /** + * @return every stored {@link org.tomahawk.libtomahawk.collection.Playlist} in the database + */ + public List getPlaylists() { + final List playListList = new ArrayList<>(); + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID}; + + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " != ?", + new String[]{LOVEDITEMS_PLAYLIST_ID}, null, null, null); + playlistsCursor.moveToFirst(); + while (!playlistsCursor.isAfterLast()) { + Playlist playlist = getEmptyPlaylist(playlistsCursor.getString(0)); + if (playlist != null) { + playListList.add(playlist); + } + playlistsCursor.moveToNext(); + } + playlistsCursor.close(); + Collections.sort(playListList, new PlaylistComparator()); + return playListList; + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the playlist's hatchet id, null if playlist not found + */ + public String getPlaylistHatchetId(String playlistId) { + if (playlistId == null) { + return null; + } + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID}; + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}, null, null, null); + String hatchetId = null; + if (playlistsCursor.moveToFirst()) { + hatchetId = playlistsCursor.getString(0); + } + playlistsCursor.close(); + return hatchetId; + } + + /** + * @param hatchetId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the playlist's local id, null if playlist not found + */ + public String getPlaylistLocalId(String hatchetId) { + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID}; + + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID + " = ?", + new String[]{hatchetId}, null, null, null); + String localId = null; + if (playlistsCursor.moveToFirst()) { + localId = playlistsCursor.getString(0); + } + playlistsCursor.close(); + return localId; + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the playlist's name, null if playlist not found + */ + public String getPlaylistName(String playlistId) { + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME}; + + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}, null, null, null); + String name = null; + if (playlistsCursor.moveToFirst()) { + name = playlistsCursor.getString(0); + } + playlistsCursor.close(); + return name; + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the stored {@link org.tomahawk.libtomahawk.collection.Playlist} with playlistId as + * its id + */ + public Playlist getEmptyPlaylist(String playlistId) { + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TOPARTISTS}; + + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}, null, null, null); + if (playlistsCursor.moveToFirst()) { + Playlist playlist = Playlist.get(playlistId); + playlist.setName(playlistsCursor.getString(0)); + playlist.setCurrentRevision(playlistsCursor.getString(1)); + playlist.setHatchetId(playlistsCursor.getString(2)); + String rawTopArtistsString = playlistsCursor.getString(3); + if (rawTopArtistsString != null && rawTopArtistsString.length() > 0) { + playlist.setTopArtistNames(rawTopArtistsString.split("\t\t")); + } + playlistsCursor.close(); + playlist.setCount(getPlaylistTrackCount(playlistId)); + return playlist; + } + playlistsCursor.close(); + return null; + } + + public Playlist getLovedItemsPlaylist() { + return getPlaylist(LOVEDITEMS_PLAYLIST_ID, true); + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the stored {@link org.tomahawk.libtomahawk.collection.Playlist} with playlistId as + * its id + */ + public Playlist getPlaylist(String playlistId) { + return getPlaylist(playlistId, false); + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the stored {@link org.tomahawk.libtomahawk.collection.Playlist} with playlistId as + * its id + */ + private Playlist getPlaylist(String playlistId, boolean reverseEntries) { + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_NAME, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_CURRENTREVISION, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_HATCHETID, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TOPARTISTS}; + + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}, null, null, null); + if (playlistsCursor.moveToFirst()) { + columns = new String[]{TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME, + TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME, + TomahawkSQLiteHelper.TRACKS_COLUMN_ALBUMNAME, + TomahawkSQLiteHelper.TRACKS_COLUMN_RESULTHINT, + TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYID}; + Cursor tracksCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_TRACKS, columns, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ?", + new String[]{playlistId}, null, null, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYINDEX + (reverseEntries + ? " DESC" : " ASC")); + ArrayList entries = new ArrayList<>(); + tracksCursor.moveToFirst(); + while (!tracksCursor.isAfterLast()) { + String trackName = tracksCursor.getString(0); + String artistName = tracksCursor.getString(1); + String albumName = tracksCursor.getString(2); + String resultHint = tracksCursor.getString(3); + Query query = Query.get(trackName, albumName, artistName, resultHint, false, + tracksCursor.getInt(4) == TRUE); + String entryId; + if (tracksCursor.getString(5) != null) { + entryId = tracksCursor.getString(5); + } else { + entryId = IdGenerator.getLifetimeUniqueStringId(); + } + PlaylistEntry entry = PlaylistEntry.get(playlistId, query, entryId); + entries.add(entry); + tracksCursor.moveToNext(); + } + Playlist playlist = Playlist.fromEntryList(playlistId, + playlistsCursor.getString(0), playlistsCursor.getString(1), entries); + playlist.setHatchetId(playlistsCursor.getString(2)); + playlist.setFilled(true); + tracksCursor.close(); + String rawTopArtistsString = playlistsCursor.getString(3); + if (rawTopArtistsString != null && rawTopArtistsString.length() > 0) { + playlist.setTopArtistNames(rawTopArtistsString.split("\t\t")); + } + playlistsCursor.close(); + playlist.setCount(getPlaylistTrackCount(playlistId)); + return playlist; + } + playlistsCursor.close(); + return null; + } + + /** + * @param playlistId the id by which to get the correct {@link org.tomahawk.libtomahawk.collection.Playlist} + * @return the stored {@link org.tomahawk.libtomahawk.collection.Playlist} with playlistId as + * its id + */ + public long getPlaylistTrackCount(String playlistId) { + long trackCount = -1; + String[] columns = new String[]{TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT}; + Cursor playlistsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + columns, TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}, null, null, null); + if (playlistsCursor.moveToFirst()) { + if (playlistsCursor.isNull(0)) { + // if no trackcount is stored, we calculate it and store it + trackCount = DatabaseUtils.queryNumEntries(mDatabase, + TomahawkSQLiteHelper.TABLE_TRACKS, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ?", + new String[]{playlistId}); + mDatabase.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT, trackCount); + mDatabase.update(TomahawkSQLiteHelper.TABLE_PLAYLISTS, values, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } else { + trackCount = playlistsCursor.getLong(0); + } + } + playlistsCursor.close(); + return trackCount; + } + + /** + * Delete the {@link org.tomahawk.libtomahawk.collection.Playlist} with the given id + * + * @param playlistId String containing the id of the {@link org.tomahawk.libtomahawk.collection.Playlist} + * to be deleted + */ + public void deletePlaylist(final String playlistId) { + mDatabase.beginTransaction(); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_TRACKS, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ?", + new String[]{playlistId}); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_PLAYLISTS, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlistId; + EventBus.getDefault().post(event); + } + + /** + * Delete the {@link org.tomahawk.libtomahawk.collection.PlaylistEntry} with the given key in + * the {@link org.tomahawk.libtomahawk.collection.Playlist} with the given playlistId + */ + public void deleteEntryInPlaylist(final String playlistId, final String entryId) { + long trackCount = getPlaylistTrackCount(playlistId); + mDatabase.beginTransaction(); + trackCount -= mDatabase.delete(TomahawkSQLiteHelper.TABLE_TRACKS, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ? AND " + + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYID + + " = ?", new String[]{playlistId, entryId}); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT, trackCount); + mDatabase.update(TomahawkSQLiteHelper.TABLE_PLAYLISTS, values, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlistId; + EventBus.getDefault().post(event); + } + + /** + * Add the given {@link ArrayList} of {@link Track}s to the {@link + * org.tomahawk.libtomahawk.collection.Playlist} with the given playlistId + */ + public void addQueriesToPlaylist(final String playlistId, final ArrayList queries) { + long trackCount = getPlaylistTrackCount(playlistId); + + mDatabase.beginTransaction(); + // Store every single Track in the database and store the relationship + // by storing the playlists's id with it + for (Query query : queries) { + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID, + playlistId); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME, + query.getBasicTrack().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME, + query.getBasicTrack().getArtist().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ALBUMNAME, + query.getBasicTrack().getAlbum().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_RESULTHINT, + query.getTopTrackResultKey()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYINDEX, + trackCount); + if (query.isFetchedViaHatchet()) { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + TRUE); + } else { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + FALSE); + } + if (mDatabase.insert(TomahawkSQLiteHelper.TABLE_TRACKS, null, values) != -1) { + trackCount++; + } + } + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT, trackCount); + mDatabase.update(TomahawkSQLiteHelper.TABLE_PLAYLISTS, values, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlistId; + EventBus.getDefault().post(event); + } + + /** + * Add the given {@link ArrayList} of {@link Track}s to the {@link + * org.tomahawk.libtomahawk.collection.Playlist} with the given playlistId + */ + public void addEntriesToPlaylist(final String playlistId, + final ArrayList entries) { + long trackCount = getPlaylistTrackCount(playlistId); + + mDatabase.beginTransaction(); + // Store every single Track in the database and store the relationship + // by storing the playlists's id with it + for (PlaylistEntry entry : entries) { + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID, + playlistId); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME, + entry.getQuery().getBasicTrack().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME, + entry.getQuery().getBasicTrack().getArtist().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ALBUMNAME, + entry.getQuery().getBasicTrack().getAlbum().getName()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_RESULTHINT, + entry.getQuery().getTopTrackResultKey()); + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYINDEX, + trackCount); + if (entry.getQuery().isFetchedViaHatchet()) { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + TRUE); + } else { + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_ISFETCHEDVIAHATCHET, + FALSE); + } + values.put(TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTENTRYID, + entry.getId()); + if (mDatabase.insert(TomahawkSQLiteHelper.TABLE_TRACKS, null, values) != -1) { + trackCount++; + } + } + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.PLAYLISTS_COLUMN_TRACKCOUNT, trackCount); + mDatabase.update(TomahawkSQLiteHelper.TABLE_PLAYLISTS, values, + TomahawkSQLiteHelper.PLAYLISTS_COLUMN_ID + " = ?", + new String[]{playlistId}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = playlistId; + EventBus.getDefault().post(event); + } + + /** + * Checks if a query with the same track/artistName as the given query is included in the + * lovedItems Playlist + * + * @return whether or not the given query is loved + */ + public boolean isItemLoved(Query query) { + String[] columns = new String[]{TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME, + TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME}; + + Cursor tracksCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_TRACKS, columns, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ?", + new String[]{LOVEDITEMS_PLAYLIST_ID}, null, null, null + ); + tracksCursor.moveToFirst(); + while (!tracksCursor.isAfterLast()) { + String trackName = tracksCursor.getString(0); + String artistName = tracksCursor.getString(1); + if (query.getName().equalsIgnoreCase(trackName) + && query.getArtist().getName().equalsIgnoreCase(artistName)) { + tracksCursor.close(); + return true; + } + tracksCursor.moveToNext(); + } + tracksCursor.close(); + return false; + } + + /** + * Store the given query as a lovedItem, if isLoved is true. Otherwise remove(unlove) the + * query. + */ + public void setLovedItem(Query query, boolean isLoved) { + if (isLoved) { + ArrayList queries = new ArrayList<>(); + queries.add(query); + addQueriesToPlaylist(LOVEDITEMS_PLAYLIST_ID, queries); + } else { + mDatabase.beginTransaction(); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_TRACKS, + TomahawkSQLiteHelper.TRACKS_COLUMN_PLAYLISTID + " = ? AND " + + TomahawkSQLiteHelper.TRACKS_COLUMN_TRACKNAME + " = ? AND " + + TomahawkSQLiteHelper.TRACKS_COLUMN_ARTISTNAME + " = ?", + new String[]{LOVEDITEMS_PLAYLIST_ID, query.getName(), + query.getArtist().getName()}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = LOVEDITEMS_PLAYLIST_ID; + EventBus.getDefault().post(event); + } + } + + public Cursor getSearchHistoryCursor(String entry) { + return mDatabase.query(TomahawkSQLiteHelper.TABLE_SEARCHHISTORY, null, + TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY + " LIKE ?", + new String[]{entry + "%"}, null, null, + TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ID + " DESC"); + } + + public void addEntryToSearchHistory(String entry) { + ContentValues values = new ContentValues(); + + mDatabase.beginTransaction(); + values.put(TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY, entry.trim()); + mDatabase.insert(TomahawkSQLiteHelper.TABLE_SEARCHHISTORY, null, values); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + /** + * Add an operation to the log. This operation log is being used to store pending operations, so + * that this operation can be executed, if we have the opportunity to do so. + * + * @param opToLog InfoRequestData object containing the type of the operation, which + * determines where and how to send the data to the API. Contains also the + * JSON-String which contains the data to send. + * @param timeStamp a timestamp indicating when this operation has been added to the oplog + */ + public void addOpToInfoSystemOpLog(InfoRequestData opToLog, int timeStamp) { + ContentValues values = new ContentValues(); + + mDatabase.beginTransaction(); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_TYPE, opToLog.getType()); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_HTTPTYPE, opToLog.getHttpType()); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_TIMESTAMP, timeStamp); + if (opToLog.getJsonStringToSend() != null) { + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_JSONSTRING, + opToLog.getJsonStringToSend()); + } + if (opToLog.getQueryParams() != null) { + String paramsJsonString = GsonHelper.get().toJson(opToLog.getQueryParams()); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_PARAMS, paramsJsonString); + } + mDatabase.insert(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOG, null, values); + long logCount = getLoggedOpsCount(); + values = new ContentValues(); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT, logCount + 1); + mDatabase.update(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOGINFO, values, null, null); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + /** + * Remove the operation with the given id from the InfoSystem-OpLog table + * + * @param loggedOp the opLog that should be removed from the InfoSystem-OpLog table + */ + public void removeOpFromInfoSystemOpLog(InfoRequestData loggedOp) { + mDatabase.beginTransaction(); + int deletedLogs = mDatabase.delete(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOG, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_ID + " = ?", + new String[]{String.valueOf(loggedOp.getLoggedOpId())}); + long logCount = getLoggedOpsCount(); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT, + logCount - deletedLogs); + mDatabase.update(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOGINFO, values, null, null); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + /** + * Remove the operation with the given id from the InfoSystem-OpLog table + * + * @param loggedOps a list of all the operations to remove from the InfoSystem-OpLog table + */ + public void removeOpsFromInfoSystemOpLog(List loggedOps) { + mDatabase.beginTransaction(); + int deletedLogs = 0; + for (InfoRequestData loggedOp : loggedOps) { + deletedLogs = mDatabase.delete(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOG, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_ID + " = ?", + new String[]{String.valueOf(loggedOp.getLoggedOpId())}); + } + long logCount = getLoggedOpsCount(); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT, + logCount - deletedLogs); + mDatabase.update(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOGINFO, values, null, null); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + /** + * @return an InfoRequestData object that contains all data that should be delivered to the API + */ + public List getLoggedOps() { + List loggedOps = new ArrayList<>(); + String[] columns = new String[]{TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_ID, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_TYPE, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_HTTPTYPE, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_JSONSTRING, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_PARAMS}; + + Cursor opLogCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOG, + columns, null, null, null, null, + TomahawkSQLiteHelper.INFOSYSTEMOPLOG_COLUMN_TIMESTAMP + " DESC"); + opLogCursor.moveToFirst(); + while (!opLogCursor.isAfterLast()) { + String requestId = IdGenerator.getSessionUniqueStringId(); + String paramJsonString = opLogCursor.getString(4); + QueryParams params = null; + if (paramJsonString != null) { + params = GsonHelper.get().fromJson(paramJsonString, QueryParams.class); + } + InfoRequestData infoRequestData = new InfoRequestData(requestId, opLogCursor.getInt(1), + params, opLogCursor.getInt(0), opLogCursor.getInt(2), opLogCursor.getString(3), + true); + loggedOps.add(infoRequestData); + opLogCursor.moveToNext(); + } + opLogCursor.close(); + return loggedOps; + } + + /** + * @return the count of all logged ops that should be delivered to the API + */ + public long getLoggedOpsCount() { + long logCount; + String[] columns = new String[]{TomahawkSQLiteHelper.INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT}; + Cursor opLogsCursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOGINFO, + columns, null, null, null, null, null); + if (!opLogsCursor.moveToFirst() || opLogsCursor.isNull(0)) { + // if no logcount is stored, we calculate it and store it + logCount = DatabaseUtils + .queryNumEntries(mDatabase, TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOG); + mDatabase.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT, logCount); + mDatabase.update(TomahawkSQLiteHelper.TABLE_INFOSYSTEMOPLOGINFO, values, null, + null); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } else { + logCount = opLogsCursor.getLong(0); + } + opLogsCursor.close(); + return logCount; + } + + private static void safePut(ContentValues values, String key, String value) { + if (value == null) { + values.putNull(key); + } else { + values.put(key, value); + } + } + + /** + * Add the given {@link MediaWrapper}s to the database. + * + * @param mws which you like to add to the database + */ + public synchronized void addMedias(List mws) { + ContentValues values = new ContentValues(); + + mDatabase.beginTransaction(); + for (MediaWrapper mw : mws) { + values.put(TomahawkSQLiteHelper.MEDIA_LOCATION, mw.getLocation()); + values.put(TomahawkSQLiteHelper.MEDIA_TIME, mw.getTime()); + values.put(TomahawkSQLiteHelper.MEDIA_LENGTH, mw.getLength()); + values.put(TomahawkSQLiteHelper.MEDIA_TYPE, mw.getType()); + values.put(TomahawkSQLiteHelper.MEDIA_TITLE, mw.getTitle()); + safePut(values, TomahawkSQLiteHelper.MEDIA_ARTIST, mw.getArtist()); + safePut(values, TomahawkSQLiteHelper.MEDIA_GENRE, mw.getGenre()); + safePut(values, TomahawkSQLiteHelper.MEDIA_ALBUM, mw.getAlbum()); + safePut(values, TomahawkSQLiteHelper.MEDIA_ALBUMARTIST, mw.getAlbumArtist()); + values.put(TomahawkSQLiteHelper.MEDIA_WIDTH, mw.getWidth()); + values.put(TomahawkSQLiteHelper.MEDIA_HEIGHT, mw.getHeight()); + values.put(TomahawkSQLiteHelper.MEDIA_ARTWORKURL, mw.getArtworkURL()); + values.put(TomahawkSQLiteHelper.MEDIA_AUDIOTRACK, mw.getAudioTrack()); + values.put(TomahawkSQLiteHelper.MEDIA_SPUTRACK, mw.getSpuTrack()); + values.put(TomahawkSQLiteHelper.MEDIA_TRACKNUMBER, mw.getTrackNumber()); + values.put(TomahawkSQLiteHelper.MEDIA_DISCNUMBER, mw.getDiscNumber()); + values.put(TomahawkSQLiteHelper.MEDIA_LASTMODIFIED, mw.getLastModified()); + mDatabase.replace(TomahawkSQLiteHelper.TABLE_MEDIA, "NULL", values); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + } + + public synchronized HashMap getMedias() { + Cursor cursor; + HashMap medias = new HashMap<>(); + int chunk_count = 0; + int count; + + do { + count = 0; + cursor = mDatabase.rawQuery(String.format(Locale.US, + "SELECT %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s FROM %s LIMIT %d OFFSET %d", + TomahawkSQLiteHelper.MEDIA_LOCATION, //0 string + TomahawkSQLiteHelper.MEDIA_TIME, //1 long + TomahawkSQLiteHelper.MEDIA_LENGTH, //2 long + TomahawkSQLiteHelper.MEDIA_TYPE, //3 int + TomahawkSQLiteHelper.MEDIA_TITLE, //4 string + TomahawkSQLiteHelper.MEDIA_ARTIST, //5 string + TomahawkSQLiteHelper.MEDIA_GENRE, //6 string + TomahawkSQLiteHelper.MEDIA_ALBUM, //7 string + TomahawkSQLiteHelper.MEDIA_ALBUMARTIST, //8 string + TomahawkSQLiteHelper.MEDIA_WIDTH, //9 int + TomahawkSQLiteHelper.MEDIA_HEIGHT, //10 int + TomahawkSQLiteHelper.MEDIA_ARTWORKURL, //11 string + TomahawkSQLiteHelper.MEDIA_AUDIOTRACK, //12 int + TomahawkSQLiteHelper.MEDIA_SPUTRACK, //13 int + TomahawkSQLiteHelper.MEDIA_TRACKNUMBER, // 14 int + TomahawkSQLiteHelper.MEDIA_DISCNUMBER, //15 int + TomahawkSQLiteHelper.MEDIA_LASTMODIFIED, //16 long + TomahawkSQLiteHelper.TABLE_MEDIA, + CHUNK_SIZE, + chunk_count * CHUNK_SIZE), null); + + if (cursor.moveToFirst()) { + try { + do { + final Uri uri = AndroidUtil.LocationToUri(cursor.getString(0)); + MediaWrapper media = new MediaWrapper(uri, + cursor.getLong(1), // MEDIA_TIME + cursor.getLong(2), // MEDIA_LENGTH + cursor.getInt(3), // MEDIA_TYPE + null, // MEDIA_PICTURE + cursor.getString(4), // MEDIA_TITLE + cursor.getString(5), // MEDIA_ARTIST + cursor.getString(6), // MEDIA_GENRE + cursor.getString(7), // MEDIA_ALBUM + cursor.getString(8), // MEDIA_ALBUMARTIST + cursor.getInt(9), // MEDIA_WIDTH + cursor.getInt(10), // MEDIA_HEIGHT + cursor.getString(11), // MEDIA_ARTWORKURL + cursor.getInt(12), // MEDIA_AUDIOTRACK + cursor.getInt(13), // MEDIA_SPUTRACK + cursor.getInt(14), // MEDIA_TRACKNUMBER + cursor.getInt(15), // MEDIA_DISCNUMBER + cursor.getLong(16)); // MEDIA_LAST_MODIFIED + medias.put(media.getUri().toString(), media); + + count++; + } while (cursor.moveToNext()); + } catch (IllegalStateException e) { + //Google bug causing IllegalStateException, see + //https://code.google.com/p/android/issues/detail?id=32472 + } + } + + cursor.close(); + chunk_count++; + } while (count == CHUNK_SIZE); + + return medias; + } + + public synchronized void removeMedias(Set locations) { + mDatabase.beginTransaction(); + try { + for (String location : locations) { + mDatabase.delete(TomahawkSQLiteHelper.TABLE_MEDIA, + TomahawkSQLiteHelper.MEDIA_LOCATION + "=?", new String[]{location}); + } + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } + + public synchronized void removeAllMedias() { + mDatabase.beginTransaction(); + try { + mDatabase.delete(TomahawkSQLiteHelper.TABLE_MEDIA, "1", null); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } + + public synchronized boolean isMediaDirComplete(String path) { + Cursor cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + new String[]{TomahawkSQLiteHelper.MEDIADIRS_PATH}, + TomahawkSQLiteHelper.MEDIADIRS_PATH + " LIKE ? || '_%'", + new String[]{path}, null, null, null); + boolean exists = cursor.moveToFirst(); + cursor.close(); + return !exists; + } + + public synchronized boolean isMediaDirWhiteListed(String path) { + Cursor cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + new String[]{TomahawkSQLiteHelper.MEDIADIRS_PATH}, + TomahawkSQLiteHelper.MEDIADIRS_PATH + "= ? AND " + + TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED + "= ?", + new String[]{path, String.valueOf(FALSE)}, null, null, null); + boolean isWhitelisted = cursor.moveToFirst(); + cursor.close(); + if (!isWhitelisted) { + cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + new String[]{TomahawkSQLiteHelper.MEDIADIRS_PATH}, + "? LIKE " + TomahawkSQLiteHelper.MEDIADIRS_PATH + " || '%' AND " + + TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED + "= ?", + new String[]{path, String.valueOf(FALSE)}, null, null, null); + isWhitelisted = cursor.moveToFirst(); + if (isWhitelisted) { + List paths = new ArrayList<>(); + while (!cursor.isAfterLast()) { + paths.add(cursor.getString(0)); + cursor.moveToNext(); + } + cursor.close(); + int wlDrillDownLevel = getMaxSlashCount(paths); + cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + new String[]{TomahawkSQLiteHelper.MEDIADIRS_PATH}, + "? LIKE " + TomahawkSQLiteHelper.MEDIADIRS_PATH + " || '%' AND " + + TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED + "= ?", + new String[]{path, String.valueOf(TRUE)}, null, null, null); + if (!cursor.moveToFirst()) { + isWhitelisted = true; + } else { + paths = new ArrayList<>(); + while (!cursor.isAfterLast()) { + paths.add(cursor.getString(0)); + cursor.moveToNext(); + } + cursor.close(); + int blDrillDownLevel = getMaxSlashCount(paths); + isWhitelisted = wlDrillDownLevel > blDrillDownLevel; + } + } + cursor.close(); + } + return isWhitelisted; + } + + private static int getSlashCount(String string) { + int count = 0; + char[] pathChars = string.toCharArray(); + for (char pathChar : pathChars) { + if (pathChar == '/') { + count++; + } + } + return count; + } + + private static int getMaxSlashCount(List strings) { + int maxCount = 0; + for (String string : strings) { + maxCount = Math.max(maxCount, getSlashCount(string)); + } + return maxCount; + } + + public synchronized void addMediaDir(String path) { + Log.d(TAG, "Adding mediaDir: " + path); + mDatabase.beginTransaction(); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + TomahawkSQLiteHelper.MEDIADIRS_PATH + " LIKE ? || '%'", new String[]{path}); + Log.d(TAG, "Removed mediaDir from white/blacklist: " + path); + if (!isMediaDirWhiteListed(path)) { + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.MEDIADIRS_PATH, path); + values.put(TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED, FALSE); + mDatabase.insert(TomahawkSQLiteHelper.TABLE_MEDIADIRS, null, values); + Log.d(TAG, "Added mediaDir to whitelist: " + path); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + public synchronized void removeMediaDir(String path) { + Log.d(TAG, "Removing mediaDir: " + path); + mDatabase.beginTransaction(); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + TomahawkSQLiteHelper.MEDIADIRS_PATH + " LIKE ? || '%'", new String[]{path}); + Log.d(TAG, "Removed mediaDir from white/blacklist: " + path); + if (isMediaDirWhiteListed(path)) { + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.MEDIADIRS_PATH, path); + values.put(TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED, TRUE); + mDatabase.insert(TomahawkSQLiteHelper.TABLE_MEDIADIRS, null, values); + Log.d(TAG, "Added mediaDir to blacklist: " + path); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + public synchronized List getMediaDirs(boolean blacklisted) { + Cursor cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_MEDIADIRS, + new String[]{TomahawkSQLiteHelper.MEDIADIRS_PATH}, + TomahawkSQLiteHelper.MEDIADIRS_BLACKLISTED + "= ?", + new String[]{String.valueOf(blacklisted ? TRUE : FALSE)}, + null, null, null); + cursor.moveToFirst(); + List paths = new ArrayList<>(); + if (!cursor.isAfterLast()) { + do { + File dir = new File(cursor.getString(0)); + paths.add(dir); + } while (cursor.moveToNext()); + } + cursor.close(); + return paths; + } + + public synchronized void storeStation(StationPlaylist stationPlaylist) { + mDatabase.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(TomahawkSQLiteHelper.STATIONS_COLUMN_ID, stationPlaylist.getCacheKey()); + values.put(TomahawkSQLiteHelper.STATIONS_COLUMN_JSON, stationPlaylist.toJson()); + values.put(TomahawkSQLiteHelper.STATIONS_COLUMN_CREATEDTIMESTAMP, + stationPlaylist.getCreatedTimeStamp()); + values.put(TomahawkSQLiteHelper.STATIONS_COLUMN_PLAYEDTIMESTAMP, + stationPlaylist.getPlayedTimeStamp()); + mDatabase.insertWithOnConflict(TomahawkSQLiteHelper.TABLE_STATIONS, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = stationPlaylist.getId(); + EventBus.getDefault().post(event); + } + + public synchronized void deleteStation(StationPlaylist stationPlaylist) { + mDatabase.beginTransaction(); + mDatabase.delete(TomahawkSQLiteHelper.TABLE_STATIONS, + TomahawkSQLiteHelper.STATIONS_COLUMN_ID + " = ?", + new String[]{stationPlaylist.getCacheKey()}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + PlaylistsUpdatedEvent event = new PlaylistsUpdatedEvent(); + event.mPlaylistId = stationPlaylist.getId(); + EventBus.getDefault().post(event); + } + + public synchronized List getStations() { + Cursor cursor = mDatabase.query(TomahawkSQLiteHelper.TABLE_STATIONS, + new String[]{TomahawkSQLiteHelper.STATIONS_COLUMN_JSON, + TomahawkSQLiteHelper.STATIONS_COLUMN_CREATEDTIMESTAMP, + TomahawkSQLiteHelper.STATIONS_COLUMN_PLAYEDTIMESTAMP}, + null, null, null, null, + TomahawkSQLiteHelper.STATIONS_COLUMN_CREATEDTIMESTAMP + " DESC"); + cursor.moveToFirst(); + List stations = new ArrayList<>(); + if (!cursor.isAfterLast()) { + do { + String json = cursor.getString(0); + StationPlaylist station = StationPlaylist.get(json); + station.setCreatedTimeStamp(cursor.getLong(1)); + station.setPlayedTimeStamp(cursor.getLong(2)); + stations.add(station); + } while (cursor.moveToNext()); + } + cursor.close(); + return stations; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/database/TomahawkSQLiteHelper.java b/app/src/main/java/org/tomahawk/libtomahawk/database/TomahawkSQLiteHelper.java new file mode 100644 index 000000000..4fbfd4825 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/database/TomahawkSQLiteHelper.java @@ -0,0 +1,329 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; +import android.util.Log; + +/** + * This is a helper class to declare the different column names inside our database, and to create + * and call the proper SQL commands onCreate and onUpgrade + */ +public class TomahawkSQLiteHelper extends SQLiteOpenHelper { + + public static final String TAG = TomahawkSQLiteHelper.class.getSimpleName(); + + public static final String TABLE_STATIONS = "stations"; + + public static final String STATIONS_COLUMN_ID = "id"; + + public static final String STATIONS_COLUMN_JSON = "json"; + + public static final String STATIONS_COLUMN_CREATEDTIMESTAMP = "createdtimestamp"; + + public static final String STATIONS_COLUMN_PLAYEDTIMESTAMP = "playedtimestamp"; + + public static final String TABLE_PLAYLISTS = "playlists"; + + public static final String PLAYLISTS_COLUMN_ID = "id"; + + public static final String PLAYLISTS_COLUMN_NAME = "name"; + + public static final String PLAYLISTS_COLUMN_CURRENTTRACKINDEX = "currenttrackindex"; + + public static final String PLAYLISTS_COLUMN_CURRENTREVISION = "currentrevision"; + + public static final String PLAYLISTS_COLUMN_HATCHETID = "hatchetid"; + + public static final String PLAYLISTS_COLUMN_TOPARTISTS = "topartists"; + + public static final String PLAYLISTS_COLUMN_TRACKCOUNT = "trackcount"; + + public static final String TABLE_TRACKS = "tracks"; + + public static final String TRACKS_COLUMN_ID = "id"; + + public static final String TRACKS_COLUMN_PLAYLISTID = "playlistid"; + + public static final String TRACKS_COLUMN_TRACKNAME = "trackname"; + + public static final String TRACKS_COLUMN_ARTISTNAME = "artistname"; + + public static final String TRACKS_COLUMN_ALBUMNAME = "albumname"; + + public static final String TRACKS_COLUMN_RESULTHINT = "resulthint"; + + public static final String TRACKS_COLUMN_ISFETCHEDVIAHATCHET = "isfetchedviahatchet"; + + public static final String TRACKS_COLUMN_PLAYLISTENTRYID = "playlistentryid"; + + public static final String TRACKS_COLUMN_PLAYLISTENTRYINDEX = "playlistentryindex"; + + public static final String TABLE_SEARCHHISTORY = "searchhistory"; + + public static final String SEARCHHISTORY_COLUMN_ID = BaseColumns._ID; + + public static final String SEARCHHISTORY_COLUMN_ENTRY = "entry"; + + public static final String TABLE_INFOSYSTEMOPLOGINFO = "infosystemoploginfo"; + + public static final String INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT = "logcount"; + + public static final String TABLE_INFOSYSTEMOPLOG = "infosystemoplog"; + + public static final String INFOSYSTEMOPLOG_COLUMN_ID = "id"; + + public static final String INFOSYSTEMOPLOG_COLUMN_TYPE = "type"; + + public static final String INFOSYSTEMOPLOG_COLUMN_HTTPTYPE = "httptype"; + + public static final String INFOSYSTEMOPLOG_COLUMN_JSONSTRING = "jsonstring"; + + public static final String INFOSYSTEMOPLOG_COLUMN_PARAMS = "params"; + + public static final String INFOSYSTEMOPLOG_COLUMN_TIMESTAMP = "timestamp"; + + public static final String TABLE_LOVED_ALBUMS = "starred_albums"; + + public static final String LOVED_ALBUMS_COLUMN_ID = "id"; + + public static final String LOVED_ALBUMS_COLUMN_ARTISTNAME = "artistname"; + + public static final String LOVED_ALBUMS_COLUMN_ALBUMNAME = "albumname"; + + public static final String TABLE_LOVED_ARTISTS = "starred_artists"; + + public static final String LOVED_ARTISTS_COLUMN_ID = "id"; + + public static final String LOVED_ARTISTS_COLUMN_ARTISTNAME = "artistname"; + + //media data + public static final String TABLE_MEDIA = "media"; + + public static final String MEDIA_LOCATION = "location"; + + public static final String MEDIA_TIME = "time"; + + public static final String MEDIA_LENGTH = "length"; + + public static final String MEDIA_TYPE = "type"; + + public static final String MEDIA_PICTURE = "picture"; + + public static final String MEDIA_TITLE = "title"; + + public static final String MEDIA_ARTIST = "artist"; + + public static final String MEDIA_GENRE = "genre"; + + public static final String MEDIA_ALBUM = "album"; + + public static final String MEDIA_ALBUMARTIST = "albumartist"; + + public static final String MEDIA_WIDTH = "width"; + + public static final String MEDIA_HEIGHT = "height"; + + public static final String MEDIA_ARTWORKURL = "artwork_url"; + + public static final String MEDIA_AUDIOTRACK = "audio_track"; + + public static final String MEDIA_SPUTRACK = "spu_track"; + + public static final String MEDIA_LASTMODIFIED = "last_modified"; + + public static final String MEDIA_DISCNUMBER = "disc_number"; + + public static final String MEDIA_TRACKNUMBER = "track_number"; + + public enum mediaColumn { + MEDIA_TABLE_NAME, MEDIA_PATH, MEDIA_TIME, MEDIA_LENGTH, + MEDIA_TYPE, MEDIA_PICTURE, MEDIA_TITLE, MEDIA_ARTIST, MEDIA_GENRE, MEDIA_ALBUM, + MEDIA_ALBUMARTIST, MEDIA_WIDTH, MEDIA_HEIGHT, MEDIA_ARTWORKURL, MEDIA_AUDIOTRACK, + MEDIA_SPUTRACK, MEDIA_TRACKNUMBER, MEDIA_DISCNUMBER, MEDIA_LAST_MODIFIED + } + + public static final String TABLE_MEDIADIRS = "mediadirs"; + + public static final String MEDIADIRS_PATH = "path"; + + public static final String MEDIADIRS_BLACKLISTED = "blacklisted"; + + + public static final String TABLE_ALBUMS = "albums"; //Legacy + + private static final String DATABASE_NAME = "userplaylists.db"; + + private static final int DATABASE_VERSION = 20; + + // Database creation sql statements + private static final String CREATE_TABLE_PLAYLISTS = + "CREATE TABLE `" + TABLE_PLAYLISTS + "` ( `" + + PLAYLISTS_COLUMN_ID + "` TEXT PRIMARY KEY , `" + + PLAYLISTS_COLUMN_NAME + "` TEXT , `" + + PLAYLISTS_COLUMN_CURRENTTRACKINDEX + "` INTEGER , `" + + PLAYLISTS_COLUMN_CURRENTREVISION + "` TEXT , `" + + PLAYLISTS_COLUMN_HATCHETID + "` TEXT , `" + + PLAYLISTS_COLUMN_TOPARTISTS + "` TEXT , `" + + PLAYLISTS_COLUMN_TRACKCOUNT + "` INTEGER );"; + + private static final String CREATE_TABLE_TRACKS = + "CREATE TABLE `" + TABLE_TRACKS + "` ( `" + + TRACKS_COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, `" + + TRACKS_COLUMN_PLAYLISTID + "` TEXT , `" + + TRACKS_COLUMN_TRACKNAME + "` TEXT ,`" + + TRACKS_COLUMN_ARTISTNAME + "` TEXT ,`" + + TRACKS_COLUMN_ALBUMNAME + "` TEXT ,`" + + TRACKS_COLUMN_RESULTHINT + "` TEXT ,`" + + TRACKS_COLUMN_ISFETCHEDVIAHATCHET + "` INTEGER ,`" + + TRACKS_COLUMN_PLAYLISTENTRYID + "` TEXT ,`" + + TRACKS_COLUMN_PLAYLISTENTRYINDEX + "` INTEGER ," + + " FOREIGN KEY (`" + TRACKS_COLUMN_PLAYLISTID + "`)" + + " REFERENCES `" + TABLE_PLAYLISTS + "` (`" + PLAYLISTS_COLUMN_ID + + "`));"; + + private static final String CREATE_TABLE_SEARCHHISTORY = + "CREATE TABLE `" + TABLE_SEARCHHISTORY + "` ( `" + + SEARCHHISTORY_COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, `" + + SEARCHHISTORY_COLUMN_ENTRY + "` TEXT UNIQUE ON CONFLICT REPLACE);"; + + private static final String CREATE_TABLE_INFOSYSTEMOPLOGINFO = + "CREATE TABLE `" + TABLE_INFOSYSTEMOPLOGINFO + "` ( `" + + INFOSYSTEMOPLOGINFO_COLUMN_LOGCOUNT + "` INTEGER);"; + + private static final String CREATE_TABLE_INFOSYSTEMOPLOG = + "CREATE TABLE `" + TABLE_INFOSYSTEMOPLOG + "` ( `" + + INFOSYSTEMOPLOG_COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, `" + + INFOSYSTEMOPLOG_COLUMN_TYPE + "` INTEGER, `" + + INFOSYSTEMOPLOG_COLUMN_HTTPTYPE + "` INTEGER, `" + + INFOSYSTEMOPLOG_COLUMN_JSONSTRING + "` TEXT, `" + + INFOSYSTEMOPLOG_COLUMN_PARAMS + "` TEXT, `" + + INFOSYSTEMOPLOG_COLUMN_TIMESTAMP + "` INTEGER);"; + + private static final String CREATE_TABLE_LOVED_ALBUMS = + "CREATE TABLE `" + TABLE_LOVED_ALBUMS + "` ( `" + + LOVED_ALBUMS_COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, `" + + LOVED_ALBUMS_COLUMN_ARTISTNAME + "` TEXT ,`" + + LOVED_ALBUMS_COLUMN_ALBUMNAME + "` TEXT);"; + + private static final String CREATE_TABLE_LOVED_ARTISTS = + "CREATE TABLE `" + TABLE_LOVED_ARTISTS + "` ( `" + + LOVED_ARTISTS_COLUMN_ID + "` INTEGER PRIMARY KEY AUTOINCREMENT, `" + + LOVED_ARTISTS_COLUMN_ARTISTNAME + "` TEXT);"; + + private static final String CREATE_TABLE_MEDIA = "CREATE TABLE IF NOT EXISTS " + + TABLE_MEDIA + " (" + + MEDIA_LOCATION + " TEXT PRIMARY KEY NOT NULL, " + + MEDIA_TIME + " INTEGER, " + + MEDIA_LENGTH + " INTEGER, " + + MEDIA_TYPE + " INTEGER, " + + MEDIA_PICTURE + " BLOB, " + + MEDIA_TITLE + " TEXT, " + + MEDIA_ARTIST + " TEXT, " + + MEDIA_GENRE + " TEXT, " + + MEDIA_ALBUM + " TEXT, " + + MEDIA_ALBUMARTIST + " TEXT, " + + MEDIA_WIDTH + " INTEGER, " + + MEDIA_HEIGHT + " INTEGER, " + + MEDIA_ARTWORKURL + " TEXT, " + + MEDIA_AUDIOTRACK + " INTEGER, " + + MEDIA_SPUTRACK + " INTEGER, " + + MEDIA_TRACKNUMBER + " INTEGER, " + + MEDIA_DISCNUMBER + " INTEGER, " + + MEDIA_LASTMODIFIED + " INTEGER" + + ");"; + + private static final String CREATE_TABLE_MEDIADIRS = "CREATE TABLE " + + TABLE_MEDIADIRS + " (" + + MEDIADIRS_PATH + " TEXT PRIMARY KEY NOT NULL, " + + MEDIADIRS_BLACKLISTED + " INTEGER " + + ");"; + + private static final String CREATE_TABLE_STATIONS = + "CREATE TABLE `" + TABLE_STATIONS + "` ( `" + + STATIONS_COLUMN_ID + "` TEXT PRIMARY KEY, `" + + STATIONS_COLUMN_JSON + "` TEXT, `" + + STATIONS_COLUMN_CREATEDTIMESTAMP + "` INTEGER, `" + + STATIONS_COLUMN_PLAYEDTIMESTAMP + "` TEXT );"; + + public TomahawkSQLiteHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + /** + * Creates the tables + */ + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL(CREATE_TABLE_PLAYLISTS); + database.execSQL(CREATE_TABLE_TRACKS); + database.execSQL(CREATE_TABLE_SEARCHHISTORY); + database.execSQL(CREATE_TABLE_INFOSYSTEMOPLOGINFO); + database.execSQL(CREATE_TABLE_INFOSYSTEMOPLOG); + database.execSQL(CREATE_TABLE_LOVED_ALBUMS); + database.execSQL(CREATE_TABLE_LOVED_ARTISTS); + database.execSQL(CREATE_TABLE_MEDIA); + database.execSQL(CREATE_TABLE_MEDIADIRS); + database.execSQL(CREATE_TABLE_STATIONS); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + + ", which might destroy all old data"); + if (oldVersion < 11) { + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_TRACKS + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_ALBUMS + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_PLAYLISTS + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_SEARCHHISTORY + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_INFOSYSTEMOPLOGINFO + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_INFOSYSTEMOPLOG + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_LOVED_ALBUMS + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_LOVED_ARTISTS + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_MEDIA + "`;"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_MEDIADIRS + "`;"); + onCreate(db); + } else { + if (oldVersion < 13) { + db.execSQL("DROP TABLE IF EXISTS `" + CREATE_TABLE_MEDIADIRS + "`;"); + db.execSQL(CREATE_TABLE_MEDIADIRS); + } + if (oldVersion < 16) { + db.execSQL("ALTER TABLE `" + TABLE_PLAYLISTS + "` ADD COLUMN `" + + PLAYLISTS_COLUMN_TOPARTISTS + "` TEXT"); + } + if (oldVersion < 17) { + db.execSQL("ALTER TABLE `" + TABLE_PLAYLISTS + "` ADD COLUMN `" + + PLAYLISTS_COLUMN_TRACKCOUNT + "` INTEGER"); + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_INFOSYSTEMOPLOGINFO + "`;"); + db.execSQL(CREATE_TABLE_INFOSYSTEMOPLOGINFO); + } + if (oldVersion < 19) { + db.execSQL("DROP TABLE IF EXISTS `" + TABLE_MEDIA + "`;"); + db.execSQL(CREATE_TABLE_MEDIA); + } + if (oldVersion < 20) { + db.execSQL(CREATE_TABLE_STATIONS); + } + } + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/database/UserCollectionDb.java b/app/src/main/java/org/tomahawk/libtomahawk/database/UserCollectionDb.java new file mode 100644 index 000000000..c34c15e29 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/database/UserCollectionDb.java @@ -0,0 +1,168 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.database; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import java.util.List; + +public class UserCollectionDb extends CollectionDb { + + public static final String TAG = UserCollectionDb.class.getSimpleName(); + + public UserCollectionDb(Context context, String collectionId) { + super(context, collectionId); + } + + public void addArtists(List artists, List lastModifieds) { + mDb.beginTransaction(); + for (int i = 0, artistsSize = artists.size(); i < artistsSize; i++) { + Artist artist = artists.get(i); + ContentValues values = new ContentValues(); + values.put(ARTISTS_ARTIST, artist.getName()); + values.put(ARTISTS_ARTISTDISAMBIGUATION, ""); + values.put(ARTISTS_TYPE, TYPE_HATCHET_EXPLICIT); + long lastModified; + if (lastModifieds != null && i < lastModifieds.size()) { + lastModified = lastModifieds.get(i); + } else { + lastModified = Long.MAX_VALUE; + } + values.put(ARTISTS_LASTMODIFIED, lastModified); + mDb.insert(TABLE_ARTISTS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + } + + public void remove(Artist artist) { + mDb.beginTransaction(); + mDb.delete(TABLE_ARTISTS, ARTISTS_ARTIST + " = ? AND " + ARTISTS_TYPE + " = ?", + new String[]{artist.getName(), String.valueOf(TYPE_HATCHET_EXPLICIT)}); + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + } + + /** + * Checks if an artist with the same artistName as the given artist is loved + * + * @return whether or not the given artist is loved + */ + public boolean isLoved(Artist artist) { + String[] columns = new String[]{ID}; + Cursor artistsCursor = mDb.query(TABLE_ARTISTS, columns, + ARTISTS_ARTIST + " = ? AND " + ARTISTS_TYPE + " = ?", + new String[]{artist.getName(), String.valueOf(TYPE_HATCHET_EXPLICIT)}, null, null, + null); + boolean isLoved = artistsCursor.getCount() > 0; + artistsCursor.close(); + return isLoved; + } + + public void addAlbums(List albums, List lastModifieds) { + // Add the album's artist as an implicitly loved entry + mDb.beginTransaction(); + for (Album album : albums) { + ContentValues values = new ContentValues(); + values.put(ARTISTS_ARTIST, album.getArtist().getName()); + values.put(ARTISTS_ARTISTDISAMBIGUATION, ""); + values.put(ARTISTS_TYPE, TYPE_HATCHET_IMPLICIT); + values.put(ARTISTS_LASTMODIFIED, Long.MAX_VALUE); + mDb.insert(TABLE_ARTISTS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + + // Add the album as an explicitly loved entry + mDb.beginTransaction(); + for (int i = 0, albumsSize = albums.size(); i < albumsSize; i++) { + Album album = albums.get(i); + ContentValues values = new ContentValues(); + values.put(ALBUMS_ALBUM, album.getName()); + values.put(ALBUMS_ALBUMARTISTID, + getArtistId(album.getArtist().getName(), TYPE_HATCHET_IMPLICIT)); + values.put(ALBUMS_TYPE, TYPE_HATCHET_EXPLICIT); + long lastModified; + if (lastModifieds != null && i < lastModifieds.size()) { + lastModified = lastModifieds.get(i); + } else { + lastModified = Long.MAX_VALUE; + } + values.put(ALBUMS_LASTMODIFIED, lastModified); + mDb.insert(TABLE_ALBUMS, null, values); + } + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + } + + public void remove(Album album) { + mDb.beginTransaction(); + int albumArtistId = getArtistId(album.getArtist().getName(), TYPE_HATCHET_IMPLICIT); + mDb.delete(TABLE_ARTISTS, ARTISTS_ARTIST + " = ? AND " + ARTISTS_TYPE + " = ?", + new String[]{album.getArtist().getName(), + String.valueOf(TYPE_HATCHET_IMPLICIT)}); + mDb.delete(TABLE_ALBUMS, ALBUMS_ALBUM + " = ? AND " + ALBUMS_ALBUMARTISTID + " = ? AND " + + ALBUMS_TYPE + " = ?", + new String[]{album.getName(), String.valueOf(albumArtistId), + String.valueOf(TYPE_HATCHET_EXPLICIT)}); + mDb.setTransactionSuccessful(); + mDb.endTransaction(); + } + + /** + * Checks if an album with the same albumName as the given album is loved + * + * @return whether or not the given album is loved + */ + public boolean isLoved(Album album) { + int albumArtistId = getArtistId(album.getArtist().getName(), TYPE_HATCHET_IMPLICIT); + String[] columns = new String[]{ID}; + Cursor albumsCursor = mDb.query(TABLE_ALBUMS, columns, + ALBUMS_ALBUM + " = ? AND " + ALBUMS_ALBUMARTISTID + " = ? AND " + + ALBUMS_TYPE + " = ?", + new String[]{album.getName(), String.valueOf(albumArtistId), + String.valueOf(TYPE_HATCHET_EXPLICIT)}, null, null, null + ); + boolean isLoved = albumsCursor.getCount() > 0; + albumsCursor.close(); + return isLoved; + } + + private int getArtistId(String artistName, int artistType) { + Cursor cursor = null; + try { + cursor = mDb.query(TABLE_ARTISTS, new String[]{ID}, + ARTISTS_ARTIST + " = ? AND " + ARTISTS_TYPE + " = ?", + new String[]{artistName, String.valueOf(artistType)}, null, null, null); + cursor.moveToFirst(); + if (cursor.getCount() == 0) { + return -1; + } + return cursor.getInt(0); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoPlugin.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoPlugin.java new file mode 100644 index 000000000..a310d28fd --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoPlugin.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; + +public interface InfoPlugin { + + void send(InfoRequestData infoRequestData, AuthenticatorUtils authenticatorUtils); + + void resolve(InfoRequestData infoRequestData); + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoRequestData.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoRequestData.java new file mode 100644 index 000000000..26cb94b49 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoRequestData.java @@ -0,0 +1,232 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The parameter-object which is being used in the class InfoSystem to define the request and later + * on store results. + */ +public class InfoRequestData { + + private final static String TAG = InfoRequestData.class.getSimpleName(); + + public static final int INFOREQUESTDATA_TYPE_TRACKS = 400; + + public static final int INFOREQUESTDATA_TYPE_ARTISTS = 600; + + public static final int INFOREQUESTDATA_TYPE_ARTISTS_TOPHITSANDALBUMS = 601; + + public static final int INFOREQUESTDATA_TYPE_ALBUMS = 700; + + public static final int INFOREQUESTDATA_TYPE_ALBUMS_TRACKS = 701; + + public static final int INFOREQUESTDATA_TYPE_USERS = 800; + + public static final int INFOREQUESTDATA_TYPE_USERS_PLAYLISTS = 801; + + public static final int INFOREQUESTDATA_TYPE_USERS_LOVEDITEMS = 802; + + public static final int INFOREQUESTDATA_TYPE_USERS_LOVEDALBUMS = 803; + + public static final int INFOREQUESTDATA_TYPE_USERS_LOVEDARTISTS = 804; + + public static final int INFOREQUESTDATA_TYPE_USERS_PLAYBACKLOG = 805; + + public static final int INFOREQUESTDATA_TYPE_USERS_FOLLOWS = 806; + + public static final int INFOREQUESTDATA_TYPE_USERS_FOLLOWERS = 807; + + public static final int INFOREQUESTDATA_TYPE_RELATIONSHIPS = 900; + + public static final int INFOREQUESTDATA_TYPE_PLAYLISTS = 1000; + + public static final int INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES = 1001; + + public static final int INFOREQUESTDATA_TYPE_SEARCHES = 1100; + + public static final int INFOREQUESTDATA_TYPE_PLAYBACKLOGENTRIES = 1200; + + public static final int INFOREQUESTDATA_TYPE_SOCIALACTIONS = 1300; + + public static final int HTTPTYPE_GET = 0; + + public static final int HTTPTYPE_POST = 1; + + public static final int HTTPTYPE_PUT = 2; + + public static final int HTTPTYPE_DELETE = 3; + + private final String mRequestId; + + private final int mType; + + private final int mHttpType; + + private final QueryParams mQueryParams; + + private String mJsonStringToSend; + + private int mLoggedOpId; + + private boolean mIsBackgroundRequest; + + /** + * Storage member-variable. Used if one or several list of objects are the result. + */ + private Map> mResultListMap; + + /** + * Constructor to be used for an InfoRequestData object in a "resolve" InfoSystem request + * + * @param requestId the id of the to be constructed InfoRequestData + * @param type the type which specifies the request inside an InfoPlugin + * @param params optional parameters to the request + */ + public InfoRequestData(String requestId, int type, QueryParams params) { + mRequestId = requestId; + mType = type; + mQueryParams = params; + mHttpType = HTTPTYPE_GET; + } + + /** + * Constructor to be used for an InfoRequestData object in a "resolve" InfoSystem request + * + * @param requestId the id of the to be constructed InfoRequestData + * @param type the type which specifies the request inside an InfoPlugin + * @param params optional parameters to the request + * @param isBackgroundRequest boolean indicating whether or not this request should be run with + * the lowest priority (useful for sync operations) + */ + public InfoRequestData(String requestId, int type, QueryParams params, + boolean isBackgroundRequest) { + this(requestId, type, params); + + mIsBackgroundRequest = isBackgroundRequest; + } + + /** + * Constructor to be used for an InfoRequestData object in a "send" InfoSystem request + * + * @param requestId the id of the to be constructed InfoRequestData + * @param type the type which specifies the request inside an InfoPlugin + * @param params optional parameters to the request + * @param loggedOpId the id of the stored loggedOp + * @param httpType the http type (get, put, post, delete) + * @param jsonStringToSend the json string which will be sent via an InfoPlugin + */ + public InfoRequestData(String requestId, int type, QueryParams params, int loggedOpId, + int httpType, String jsonStringToSend) { + this(requestId, type, params, httpType, jsonStringToSend); + + mLoggedOpId = loggedOpId; + } + + /** + * Constructor to be used for an InfoRequestData object in a "send" InfoSystem request + * + * @param requestId the id of the to be constructed InfoRequestData + * @param type the type which specifies the request inside an InfoPlugin + * @param params optional parameters to the request + * @param loggedOpId the id of the stored loggedOp + * @param httpType the http type (get, put, post, delete) + * @param jsonStringToSend the json string which will be sent via an InfoPlugin + * @param isBackgroundRequest boolean indicating whether or not this request should be run with + * the lowest priority (useful for sync operations) + */ + public InfoRequestData(String requestId, int type, QueryParams params, int loggedOpId, + int httpType, String jsonStringToSend, boolean isBackgroundRequest) { + this(requestId, type, params, loggedOpId, httpType, jsonStringToSend); + + mIsBackgroundRequest = isBackgroundRequest; + } + + /** + * Constructor to be used for an InfoRequestData object in a "send" InfoSystem request + * + * @param requestId the id of the to be constructed InfoRequestData + * @param type the type which specifies the request inside an InfoPlugin + * @param params optional parameters to the request + * @param httpType the http type (get, put, post, delete) + * @param jsonStringToSend the json string which will be sent via an InfoPlugin + */ + public InfoRequestData(String requestId, int type, QueryParams params, int httpType, + String jsonStringToSend) { + mRequestId = requestId; + mType = type; + mQueryParams = params; + mHttpType = httpType; + mJsonStringToSend = jsonStringToSend; + } + + public String getRequestId() { + return mRequestId; + } + + public int getType() { + return mType; + } + + public int getHttpType() { + return mHttpType; + } + + public List getResultList(Class clss) { + if (mResultListMap != null) { + List objects = mResultListMap.get(clss); + if (objects != null && objects.size() > 0 && objects.get(0).getClass() == clss) { + return (List) objects; + } + } + return new ArrayList<>(); + } + + public void setResultList(List objects) { + if (objects.size() > 0 && objects.get(0) != null) { + if (mResultListMap == null) { + mResultListMap = new HashMap<>(); + } + mResultListMap.put(objects.get(0).getClass(), objects); + } + } + + public QueryParams getQueryParams() { + return mQueryParams; + } + + public String getJsonStringToSend() { + return mJsonStringToSend; + } + + public void setJsonStringToSend(String jsonStringToSend) { + mJsonStringToSend = jsonStringToSend; + } + + public int getLoggedOpId() { + return mLoggedOpId; + } + + public boolean isBackgroundRequest() { + return mIsBackgroundRequest; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoSystem.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoSystem.java new file mode 100644 index 000000000..189cc9519 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/InfoSystem.java @@ -0,0 +1,840 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.hatchet.HatchetInfoPlugin; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaybackLogEntry; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaybackLogPostStruct; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistEntries; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistEntriesPostStruct; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistEntriesRequest; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistPostStruct; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistRequest; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetRelationshipPostStruct; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetRelationshipStruct; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * The InfoSystem resolves metadata for artists and albums like album covers and artist images. + */ +public class InfoSystem { + + private static final String TAG = InfoSystem.class.getSimpleName(); + + private static class Holder { + + private static final InfoSystem instance = new InfoSystem(); + + } + + public static class OpLogIsEmptiedEvent { + + public HashSet mRequestTypes; + + public HashSet mPlaylistIds; + } + + public class ResultsEvent { + + public boolean mSuccess; + + public InfoRequestData mInfoRequestData; + } + + private final ArrayList mInfoPlugins = new ArrayList<>(); + + private final ConcurrentHashMap mSentRequests + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap mLoggedOpsMap + = new ConcurrentHashMap<>(); + + // We store "create playlists"-loggedOps separately, because we need to check whether or not all + // "create playlists"-loggedOps have been pushed to Hatchet before sending the corresponding + // playlist entries + private final ConcurrentHashMap mPlaylistsLoggedOpsMap + = new ConcurrentHashMap<>(); + + // LoggedOps waiting to be sent as soon as mPlaylistsLoggedOpsMap is empty + private final ArrayList mQueuedLoggedOps = new ArrayList<>(); + + private Query mLastPlaybackLogEntry = null; + + private Query mNowPlaying = null; + + private InfoSystem() { + mInfoPlugins.add(new HatchetInfoPlugin()); + } + + public static InfoSystem get() { + return Holder.instance; + } + + public void addInfoPlugin(InfoPlugin plugin) { + mInfoPlugins.add(plugin); + } + + public void removeInfoPlugin(InfoPlugin plugin) { + mInfoPlugins.remove(plugin); + } + + /** + * HatchetSearch the added InfoPlugins with the given keyword + * + * @return the created InfoRequestData's requestId + */ + public String resolve(String keyword) { + if (!TextUtils.isEmpty(keyword)) { + QueryParams params = new QueryParams(); + params.term = keyword; + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_SEARCHES, params); + } + return null; + } + + /** + * Fill up the given artist with metadata fetched from all added InfoPlugins + * + * @param artist the Artist to enrich with data from the InfoPlugins + * @param full true, if top-hits and albums should also be resolved + * @return the created InfoRequestData's requestId + */ + public String resolve(Artist artist, boolean full) { + if (artist != null && !TextUtils.isEmpty(artist.getName())) { + QueryParams params = new QueryParams(); + params.name = artist.getName(); + if (full) { + return resolve( + InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS_TOPHITSANDALBUMS, params); + } else { + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS, params); + } + } + return null; + } + + /** + * Fill up the given artist with metadata fetched from all added InfoPlugins + * + * @param album the Album to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolve(Album album) { + if (album != null && !TextUtils.isEmpty(album.getName())) { + QueryParams params = new QueryParams(); + params.name = album.getName(); + params.artistname = album.getArtist().getName(); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_ALBUMS_TRACKS, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolve(User user) { + if (user != null && !user.isOffline() && !TextUtils.isEmpty(user.getId())) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS, params); + } + return null; + } + + /** + * Get random users + * + * @param count the number of users to get + * @return the created InfoRequestData's requestId + */ + public String getRandomUsers(int count) { + QueryParams params = new QueryParams(); + params.random = String.valueOf(true); + params.count = String.valueOf(count); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS, params); + } + + public String resolve(Playlist playlist) { + if (playlist != null) { + QueryParams params = new QueryParams(); + params.playlist_local_id = playlist.getId(); + params.playlist_id = playlist.getHatchetId(); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES, params); + } + return null; + } + + /** + * Fetch a logged-in user's id and store it + * + * @param username the username with which to get the corresponding id + * @return the created InfoRequestData's requestId + */ + public String resolveUserId(String username) { + if (username != null) { + QueryParams params = new QueryParams(); + params.name = username; + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User for which to get the socialActions + * @param beforeDate the Date that specifies which socialActions to fetch + * @return the created InfoRequestData's requestId + */ + public String resolveSocialActions(User user, Date beforeDate) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.userid = user.getId(); + params.before_date = beforeDate; + params.limit = String.valueOf(HatchetInfoPlugin.SOCIALACTIONS_LIMIT); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_SOCIALACTIONS, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @param beforeDate the Date that specifies which socialActions to fetch + * @return the created InfoRequestData's requestId + */ + public String resolveFriendsFeed(User user, Date beforeDate) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.userid = user.getId(); + params.type = HatchetInfoPlugin.HATCHET_SOCIALACTION_PARAMTYPE_FRIENDSFEED; + params.before_date = beforeDate; + params.limit = String.valueOf(HatchetInfoPlugin.FRIENDSFEED_LIMIT); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_SOCIALACTIONS, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolvePlaybackLog(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_PLAYBACKLOG, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolveLovedItems(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDITEMS, params, true); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolveFollowings(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWS, params); + } + return null; + } + + /** + * Fill up the given user with metadata fetched from all added InfoPlugins + * + * @param user the User to enrich with data from the InfoPlugins + * @return the created InfoRequestData's requestId + */ + public String resolveFollowers(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWERS, params); + } + return null; + } + + /** + * Fetch the given user's list of loved albums + * + * @return the created InfoRequestData's requestId + */ + public String resolveLovedAlbums(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDALBUMS, params, true); + } + return null; + } + + /** + * Fetch the given user's list of loved artists + * + * @return the created InfoRequestData's requestId + */ + public String resolveLovedArtists(User user) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + return resolve(InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDARTISTS, params, true); + } + return null; + } + + public String resolvePlaylists(User user, boolean isBackgroundRequest) { + if (user != null && !user.isOffline()) { + QueryParams params = new QueryParams(); + params.ids = new ArrayList<>(); + params.ids.add(user.getId()); + String requestId = IdGenerator.getSessionUniqueStringId(); + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_USERS_PLAYLISTS, params, + isBackgroundRequest); + resolve(infoRequestData); + return infoRequestData.getRequestId(); + } + return null; + } + + /** + * Build an InfoRequestData object with the given data and order results + * + * @param type the type of the InfoRequestData object + * @param params all parameters to be given to the InfoPlugin + * @return the created InfoRequestData's requestId + */ + public String resolve(int type, QueryParams params) { + return resolve(type, params, false); + } + + /** + * Build an InfoRequestData object with the given data and order results + * + * @param type the type of the InfoRequestData object + * @param params all parameters to be given to the InfoPlugin + * @param isBackgroundRequest boolean indicating whether or not this request should be run with + * the lowest priority (useful for sync operations) + * @return the created InfoRequestData's requestId + */ + public String resolve(int type, QueryParams params, boolean isBackgroundRequest) { + String requestId = IdGenerator.getSessionUniqueStringId(); + InfoRequestData infoRequestData = new InfoRequestData(requestId, type, params, + isBackgroundRequest); + resolve(infoRequestData); + return infoRequestData.getRequestId(); + } + + /** + * Order results for the given InfoRequestData object + * + * @param infoRequestData the InfoRequestData object to fetch results for + */ + public void resolve(InfoRequestData infoRequestData) { + for (InfoPlugin infoPlugin : mInfoPlugins) { + infoPlugin.resolve(infoRequestData); + } + } + + public void sendPlaybackEntryPostStruct(AuthenticatorUtils authenticatorUtils) { + if (mNowPlaying != null && mNowPlaying != mLastPlaybackLogEntry) { + mLastPlaybackLogEntry = mNowPlaying; + long timeStamp = System.currentTimeMillis(); + HatchetPlaybackLogEntry playbackLogEntry = new HatchetPlaybackLogEntry(); + playbackLogEntry.albumString = mLastPlaybackLogEntry.getAlbum().getName(); + playbackLogEntry.artistString = mLastPlaybackLogEntry.getArtist().getName(); + if (playbackLogEntry.artistString.isEmpty()) { + playbackLogEntry.artistString = "Unknown Artist"; + } + playbackLogEntry.trackString = mLastPlaybackLogEntry.getName(); + if (playbackLogEntry.trackString.isEmpty()) { + playbackLogEntry.trackString = "Unknown Title"; + } + playbackLogEntry.timestamp = new Date(timeStamp); + HatchetPlaybackLogPostStruct playbackLogPostStruct = new HatchetPlaybackLogPostStruct(); + playbackLogPostStruct.playbackLogEntry = playbackLogEntry; + + String requestId = IdGenerator.getSessionUniqueStringId(); + String jsonString = GsonHelper.get().toJson(playbackLogPostStruct); + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYBACKLOGENTRIES, null, + InfoRequestData.HTTPTYPE_POST, jsonString); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + } + + public void sendNowPlayingPostStruct(AuthenticatorUtils authenticatorUtils, Query query) { + if (mNowPlaying != query) { + sendPlaybackEntryPostStruct(authenticatorUtils); + mNowPlaying = query; + long timeStamp = System.currentTimeMillis(); + HatchetPlaybackLogEntry playbackLogEntry = new HatchetPlaybackLogEntry(); + playbackLogEntry.albumString = query.getAlbum().getName(); + playbackLogEntry.artistString = query.getArtist().getName(); + if (playbackLogEntry.artistString.isEmpty()) { + playbackLogEntry.artistString = "Unknown Artist"; + } + playbackLogEntry.trackString = query.getName(); + if (playbackLogEntry.trackString.isEmpty()) { + playbackLogEntry.trackString = "Unknown Title"; + } + playbackLogEntry.type = "nowplaying"; + playbackLogEntry.timestamp = new Date(timeStamp); + HatchetPlaybackLogPostStruct playbackLogPostStruct = new HatchetPlaybackLogPostStruct(); + playbackLogPostStruct.playbackLogEntry = playbackLogEntry; + + String requestId = IdGenerator.getSessionUniqueStringId(); + String jsonString = GsonHelper.get().toJson(playbackLogPostStruct); + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYBACKLOGENTRIES, null, + InfoRequestData.HTTPTYPE_POST, jsonString); + send(infoRequestData, authenticatorUtils); + } + } + + public InfoRequestData buildPlaylistPostStruct(String localId, String title) { + HatchetPlaylistRequest request = new HatchetPlaylistRequest(); + request.title = title; + HatchetPlaylistPostStruct struct = new HatchetPlaylistPostStruct(); + struct.playlist = request; + + String requestId = IdGenerator.getSessionUniqueStringId(); + String jsonString = GsonHelper.get().toJson(struct); + QueryParams params = new QueryParams(); + params.playlist_local_id = localId; + return new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS, params, + InfoRequestData.HTTPTYPE_POST, jsonString); + } + + public void sendPlaylistPostStruct(AuthenticatorUtils authenticatorUtils, + String localId, String title) { + long timeStamp = System.currentTimeMillis(); + InfoRequestData infoRequestData = buildPlaylistPostStruct(localId, title); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + public InfoRequestData buildPlaylistEntriesPostStruct(String localPlaylistId, + List entries) { + HatchetPlaylistEntriesPostStruct struct = new HatchetPlaylistEntriesPostStruct(); + struct.playlistEntries = new ArrayList<>(); + for (PlaylistEntry entry : entries) { + HatchetPlaylistEntriesRequest request = new HatchetPlaylistEntriesRequest(); + request.trackString = entry.getQuery().getName(); + request.artistString = entry.getArtist().getName(); + request.albumString = entry.getAlbum().getName(); + struct.playlistEntries.add(request); + } + + String requestId = IdGenerator.getSessionUniqueStringId(); + String jsonString = GsonHelper.get().toJson(struct); + QueryParams params = new QueryParams(); + params.playlist_local_id = localPlaylistId; + return new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES, params, + InfoRequestData.HTTPTYPE_POST, jsonString); + } + + public void sendPlaylistEntriesPostStruct(AuthenticatorUtils authenticatorUtils, + String localPlaylistId, List entries) { + long timeStamp = System.currentTimeMillis(); + InfoRequestData infoRequestData = + buildPlaylistEntriesPostStruct(localPlaylistId, entries); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + public void deletePlaylist(AuthenticatorUtils authenticatorUtils, String localPlaylistId) { + long timeStamp = System.currentTimeMillis(); + String requestId = IdGenerator.getLifetimeUniqueStringId(); + QueryParams params = new QueryParams(); + params.playlist_local_id = localPlaylistId; + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS, params, + InfoRequestData.HTTPTYPE_DELETE, null); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + public void deletePlaylistEntry(AuthenticatorUtils authenticatorUtils, String localPlaylistId, + String entryId) { + long timeStamp = System.currentTimeMillis(); + String requestId = IdGenerator.getLifetimeUniqueStringId(); + QueryParams params = new QueryParams(); + params.entry_id = entryId; + params.playlist_local_id = localPlaylistId; + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES, params, + InfoRequestData.HTTPTYPE_DELETE, null); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + public void sendRelationshipPostStruct(AuthenticatorUtils authenticatorUtils, User user) { + sendRelationshipPostStruct(authenticatorUtils, user.getId(), null, null, null); + } + + public void sendRelationshipPostStruct(AuthenticatorUtils authenticatorUtils, Query query) { + sendRelationshipPostStruct(authenticatorUtils, null, query.getName(), + query.getArtist().getName(), null); + } + + public void sendRelationshipPostStruct(AuthenticatorUtils authenticatorUtils, Artist artist) { + sendRelationshipPostStruct(authenticatorUtils, null, null, artist.getName(), null); + } + + public void sendRelationshipPostStruct(AuthenticatorUtils authenticatorUtils, Album album) { + sendRelationshipPostStruct(authenticatorUtils, null, null, album.getArtist().getName(), + album.getName()); + } + + public InfoRequestData buildRelationshipPostStruct(String user, String track, String artist, + String album) { + HatchetRelationshipStruct relationship = new HatchetRelationshipStruct(); + relationship.targetUser = user; + relationship.targetTrackString = track; + relationship.targetArtistString = artist; + relationship.targetAlbumString = album; + relationship.type = user != null ? HatchetInfoPlugin.HATCHET_RELATIONSHIPS_TYPE_FOLLOW + : HatchetInfoPlugin.HATCHET_RELATIONSHIPS_TYPE_LOVE; + HatchetRelationshipPostStruct struct = new HatchetRelationshipPostStruct(); + struct.relationShip = relationship; + + String requestId = IdGenerator.getSessionUniqueStringId(); + + String jsonString = GsonHelper.get().toJson(struct); + return new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_RELATIONSHIPS, null, + InfoRequestData.HTTPTYPE_POST, jsonString); + } + + public void sendRelationshipPostStruct(AuthenticatorUtils authenticatorUtils, + String user, String track, String artist, String album) { + long timeStamp = System.currentTimeMillis(); + InfoRequestData infoRequestData = buildRelationshipPostStruct(user, track, artist, album); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + public void deleteRelationship(AuthenticatorUtils authenticatorUtils, String relationshipId) { + long timeStamp = System.currentTimeMillis(); + String requestId = IdGenerator.getSessionUniqueStringId(); + QueryParams params = new QueryParams(); + params.relationship_id = relationshipId; + InfoRequestData infoRequestData = new InfoRequestData(requestId, + InfoRequestData.INFOREQUESTDATA_TYPE_RELATIONSHIPS, params, + InfoRequestData.HTTPTYPE_DELETE, null); + DatabaseHelper.get().addOpToInfoSystemOpLog(infoRequestData, + (int) (timeStamp / 1000)); + sendLoggedOps(authenticatorUtils); + } + + /** + * Send the given InfoRequestData's data out to every service that can handle it + * + * @param infoRequestData the InfoRequestData object to fetch results for + * @param authenticatorUtils the AuthenticatorUtils object to fetch the appropriate access + * tokens + */ + private void send(InfoRequestData infoRequestData, AuthenticatorUtils authenticatorUtils) { + mSentRequests.put(infoRequestData.getRequestId(), infoRequestData); + for (InfoPlugin infoPlugin : mInfoPlugins) { + infoPlugin.send(infoRequestData, authenticatorUtils); + } + } + + /** + * Get the InfoRequestData with the given Id + */ + public InfoRequestData getSentLoggedOpById(String requestId) { + return mSentRequests.get(requestId); + } + + /** + * Method to enable InfoPlugins to report that the InfoRequestData objects with the given + * requestIds have received their results + */ + public void reportResults(InfoRequestData infoRequestData, boolean success) { + ResultsEvent event = new ResultsEvent(); + event.mInfoRequestData = infoRequestData; + event.mSuccess = success; + EventBus.getDefault().post(event); + } + + + public synchronized void sendLoggedOps(AuthenticatorUtils authenticatorUtils) { + List loggedOps = DatabaseHelper.get().getLoggedOps(); + for (InfoRequestData loggedOp : loggedOps) { + verifyLoggedOp(loggedOp); + } + loggedOps = DatabaseHelper.get().getLoggedOps(); + for (InfoRequestData loggedOp : loggedOps) { + if (!mLoggedOpsMap.containsKey(loggedOp.getLoggedOpId())) { + mLoggedOpsMap.put(loggedOp.getLoggedOpId(), loggedOp); + if (loggedOp.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES + || (loggedOp.getHttpType() == InfoRequestData.HTTPTYPE_DELETE + && loggedOp.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS)) { + mQueuedLoggedOps.add(loggedOp); + } else { + if (loggedOp.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS) { + mPlaylistsLoggedOpsMap.put(loggedOp.getLoggedOpId(), loggedOp); + } + send(loggedOp, authenticatorUtils); + } + } + } + trySendingQueuedOps(); + } + + public synchronized void onLoggedOpsSent(ArrayList doneRequestsIds, boolean discard) { + List loggedOps = new ArrayList<>(); + HashSet requestTypes = new HashSet<>(); + HashSet playlistIds = new HashSet<>(); + for (String doneRequestId : doneRequestsIds) { + if (mSentRequests.containsKey(doneRequestId)) { + InfoRequestData loggedOp = mSentRequests.get(doneRequestId); + loggedOps.add(loggedOp); + requestTypes.add(loggedOp.getType()); + if (loggedOp.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + playlistIds.add(loggedOp.getQueryParams().playlist_local_id); + } else if (loggedOp.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS) { + List results = + loggedOp.getResultList(HatchetPlaylistEntries.class); + if (results != null && results.size() > 0) { + HatchetPlaylistEntries entries = results.get(0); + if (entries != null && entries.playlists.size() > 0) { + playlistIds.add(entries.playlists.get(0).id); + DatabaseHelper.get().updatePlaylistHatchetId( + loggedOp.getQueryParams().playlist_local_id, + entries.playlists.get(0).id); + } + } + } + mLoggedOpsMap.remove(loggedOp.getLoggedOpId()); + } + } + if (discard) { + for (InfoRequestData loggedOp : loggedOps) { + mPlaylistsLoggedOpsMap.remove(loggedOp.getLoggedOpId()); + } + trySendingQueuedOps(); + DatabaseHelper.get().removeOpsFromInfoSystemOpLog(loggedOps); + if (DatabaseHelper.get().getLoggedOpsCount() == 0) { + if (!requestTypes.isEmpty()) { + OpLogIsEmptiedEvent event = new OpLogIsEmptiedEvent(); + event.mRequestTypes = requestTypes; + event.mPlaylistIds = playlistIds; + EventBus.getDefault().post(event); + } + } + } + } + + private synchronized void trySendingQueuedOps() { + if (mPlaylistsLoggedOpsMap.isEmpty()) { + while (!mQueuedLoggedOps.isEmpty()) { + InfoRequestData queuedLoggedOp = mQueuedLoggedOps.remove(0); + QueryParams params = queuedLoggedOp.getQueryParams(); + String hatchetId = DatabaseHelper.get() + .getPlaylistHatchetId(params.playlist_local_id); + if (hatchetId != null) { + if (queuedLoggedOp.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + if (queuedLoggedOp.getHttpType() == InfoRequestData.HTTPTYPE_POST) { + // Now that we know the hatchetId, we can add it to the playlistEntry + // object we POST to Hatchet + int newHatchetId = Integer.valueOf(hatchetId); + JsonElement element = GsonHelper.get().fromJson( + queuedLoggedOp.getJsonStringToSend(), JsonElement.class); + if (element.isJsonObject()) { + JsonObject object = (JsonObject) element; + JsonObject playlistEntry = object.getAsJsonObject("playlistEntry"); + if (playlistEntry != null) { + // old way of posting playlistEntries (one per request) + playlistEntry.addProperty("playlist", newHatchetId); + } else { + // new way of posting playlistEntries (all at once) + object.addProperty("playlist", newHatchetId); + } + } + queuedLoggedOp.setJsonStringToSend(GsonHelper.get().toJson(element)); + } else if (queuedLoggedOp.getHttpType() + == InfoRequestData.HTTPTYPE_DELETE) { + params.playlist_id = hatchetId; + } + } else { + params.playlist_id = hatchetId; + } + send(queuedLoggedOp, AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET)); + } else { + Log.e(TAG, "Hatchet sync - Couldn't send queued logged op, because the stored " + + "local playlist id was no longer valid"); + discardLoggedOp(queuedLoggedOp); + } + } + } + } + + private void discardLoggedOp(InfoRequestData loggedOp) { + mSentRequests.put(loggedOp.getRequestId(), loggedOp); + ArrayList doneRequestsIds = new ArrayList<>(); + doneRequestsIds.add(loggedOp.getRequestId()); + InfoSystem.get().onLoggedOpsSent(doneRequestsIds, true); + } + + /** + * Verify if the given loggedOp needs to be converted to a newer version. This is needed because + * the Hatchet API changes. + */ + private void verifyLoggedOp(InfoRequestData loggedOp) { + InfoRequestData convertedLogOp = null; + if (loggedOp.getType() == 1300) { // old v1 way of posting a socialAction + JsonElement element = + GsonHelper.get().fromJson(loggedOp.getJsonStringToSend(), JsonElement.class); + if (element instanceof JsonObject) { + JsonObject socialAction = ((JsonObject) element).getAsJsonObject("socialAction"); + String action = getAsString(socialAction, "action"); + if (action != null && action.equals("true")) { + String trackString = getAsString(socialAction, "trackString"); + String artistString = getAsString(socialAction, "artistString"); + String albumString = getAsString(socialAction, "albumString"); + convertedLogOp = buildRelationshipPostStruct( + null, trackString, artistString, albumString); + } else { + // We have to discard the loggedOp since we don't have any way of getting the + // associated relationShipId. Therefore we are unable to delete this particular + // relationship. + DatabaseHelper.get().removeOpFromInfoSystemOpLog(loggedOp); + } + } + } else if (loggedOp.getType() == 1001) { + JsonElement element = + GsonHelper.get().fromJson(loggedOp.getJsonStringToSend(), JsonElement.class); + if (element instanceof JsonObject) { + JsonElement playlist = ((JsonObject) element).get("playlist"); + if (playlist instanceof JsonObject + && !((JsonObject) element).has("playlistEntry")) { + // It's definitely an old "create playlist"-struct + String title = getAsString((JsonObject) playlist, "title"); + convertedLogOp = buildPlaylistPostStruct( + loggedOp.getQueryParams().playlist_local_id, title); + } + } + } else if (loggedOp.getType() == 1002) { + JsonElement element = + GsonHelper.get().fromJson(loggedOp.getJsonStringToSend(), JsonElement.class); + if (element instanceof JsonObject) { + JsonObject playlistEntry = ((JsonObject) element).getAsJsonObject("playlistEntry"); + if (playlistEntry != null) { + String trackString = getAsString(playlistEntry, "trackString"); + String artistString = getAsString(playlistEntry, "artistString"); + String albumString = getAsString(playlistEntry, "albumString"); + + Query query = Query.get(trackString, albumString, artistString, false, true); + PlaylistEntry entry = PlaylistEntry.get( + loggedOp.getQueryParams().playlist_local_id, query, + IdGenerator.getLifetimeUniqueStringId()); + List entries = new ArrayList<>(); + entries.add(entry); + convertedLogOp = buildPlaylistEntriesPostStruct( + loggedOp.getQueryParams().playlist_local_id, entries); + } + } + } + if (convertedLogOp != null) { + DatabaseHelper.get().removeOpFromInfoSystemOpLog(loggedOp); + DatabaseHelper.get().addOpToInfoSystemOpLog(convertedLogOp, + (int) System.currentTimeMillis() / 1000); + } + } + + private String getAsString(JsonObject object, String memberName) { + JsonElement element = object.get(memberName); + if (element != null && element.isJsonPrimitive()) { + return element.getAsString(); + } + return null; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/QueryParams.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/QueryParams.java new file mode 100644 index 000000000..3381821f2 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/QueryParams.java @@ -0,0 +1,39 @@ +package org.tomahawk.libtomahawk.infosystem; + +import java.util.ArrayList; +import java.util.Date; + +public class QueryParams { + + public ArrayList ids = null; + + public String name = null; + + public String userid = null; + + public String artistname = null; + + public String playlist_id = null; + + public String targettype = null; + + public String targetuserid = null; + + public String term = null; + + public String type = null; + + public String entry_id = null; + + public String playlist_local_id = null; + + public String relationship_id = null; + + public String random = null; + + public String count = null; + + public String limit = null; + + public Date before_date = null; +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/Relationship.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/Relationship.java new file mode 100644 index 000000000..4eb04e8aa --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/Relationship.java @@ -0,0 +1,90 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.resolver.Query; + +import java.util.Date; + +public class Relationship extends Cacheable { + + private String mType; + + private User mUser; + + private Date mDate; + + private Query mQuery; + + private Album mAlbum; + + private Artist mArtist; + + private Relationship(String id, String type, User user, Date date) { + super(Relationship.class, id); + + mType = type; + mUser = user; + mDate = date; + } + + public static Relationship get(String id, String type, User user, Date date) { + Cacheable cacheable = get(Relationship.class, id); + return cacheable != null ? (Relationship) cacheable : + new Relationship(id, type, user, date); + } + + public String getType() { + return mType; + } + + public User getUser() { + return mUser; + } + + public Date getDate() { + return mDate; + } + + public Query getQuery() { + return mQuery; + } + + public void setQuery(Query query) { + mQuery = query; + } + + public Album getAlbum() { + return mAlbum; + } + + public void setAlbum(Album album) { + mAlbum = album; + } + + public Artist getArtist() { + return mArtist; + } + + public void setArtist(Artist artist) { + mArtist = artist; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/ScriptInfoPlugin.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/ScriptInfoPlugin.java new file mode 100644 index 000000000..69b7c9a32 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/ScriptInfoPlugin.java @@ -0,0 +1,53 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; +import org.tomahawk.libtomahawk.resolver.ScriptPlugin; + +public class ScriptInfoPlugin implements InfoPlugin, ScriptPlugin { + + private ScriptAccount mScriptAccount; + + private ScriptObject mScriptObject; + + public ScriptInfoPlugin(ScriptObject scriptObject, ScriptAccount account) { + mScriptObject = scriptObject; + mScriptAccount = account; + } + + @Override + public void send(InfoRequestData infoRequestData, AuthenticatorUtils authenticatorUtils) { + } + + @Override + public void resolve(InfoRequestData infoRequestData) { + } + + @Override + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + @Override + public ScriptObject getScriptObject() { + return mScriptObject; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/SocialAction.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/SocialAction.java new file mode 100644 index 000000000..35a3a1195 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/SocialAction.java @@ -0,0 +1,184 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.resolver.Query; + +import java.util.Date; + +public class SocialAction extends Cacheable { + + private final String mId; + + private String mAction; + + private Album mAlbum; + + private Artist mArtist; + + private Date mDate; + + private Playlist mPlaylist; + + private User mTarget; + + private Query mQuery; + + private String mType; + + private User mUser; + + /** + * Construct a new {@link SocialAction} with the given id + */ + private SocialAction(String id) { + super(SocialAction.class, id); + + mId = id; + } + + /** + * Returns the {@link SocialAction} with the given id. If none exists in our static {@link + * java.util.concurrent.ConcurrentHashMap} yet, construct and add it. + * + * @return {@link SocialAction} with the given id + */ + public static SocialAction get(String id) { + Cacheable cacheable = get(SocialAction.class, id); + return cacheable != null ? (SocialAction) cacheable : new SocialAction(id); + } + + /** + * Get a SocialAction by providing its id + */ + public static SocialAction getByKey(String id) { + return (SocialAction) get(SocialAction.class, id); + } + + public String getName() { + return null; + } + + public Image getImage() { + if (mQuery.getImage() != null) { + return mQuery.getImage(); + } else if (mAlbum.getImage() != null) { + return mAlbum.getImage(); + } else if (mArtist.getImage() != null) { + return mArtist.getImage(); + } else { + return mUser.getImage(); + } + } + + public Object getTargetObject() { + if (mTarget != null) { + return mTarget; + } else if (mArtist != null) { + return mArtist; + } else if (mAlbum != null) { + return mAlbum; + } else if (mQuery != null) { + return mQuery; + } else if (mPlaylist != null) { + return mPlaylist; + } + return null; + } + + public String getId() { + return mId; + } + + public String getAction() { + return mAction; + } + + public void setAction(String action) { + mAction = action; + } + + public Album getAlbum() { + return mAlbum; + } + + public void setAlbum(Album album) { + mAlbum = album; + } + + public Artist getArtist() { + return mArtist; + } + + public void setArtist(Artist artist) { + mArtist = artist; + } + + public Date getDate() { + return mDate; + } + + public void setDate(Date date) { + mDate = date; + } + + public Playlist getPlaylist() { + return mPlaylist; + } + + public void setPlaylist(Playlist playlist) { + mPlaylist = playlist; + } + + public User getTarget() { + return mTarget; + } + + public void setTarget(User target) { + mTarget = target; + } + + public Query getQuery() { + return mQuery; + } + + public void setQuery(Query query) { + mQuery = query; + } + + public String getType() { + return mType; + } + + public void setType(String type) { + mType = type; + } + + public User getUser() { + return mUser; + } + + public void setUser(User user) { + mUser = user; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/User.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/User.java new file mode 100644 index 000000000..60e93e6aa --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/User.java @@ -0,0 +1,423 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem; + +import org.jdeferred.DoneCallback; +import org.jdeferred.FailCallback; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.AlphaComparable; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.fragments.SocialActionsFragment; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +public class User extends Cacheable implements AlphaComparable { + + private static User mSelf = new User("self"); + + static { + mSelf.setName("Myself"); + mSelf.setIsOffline(true); + } + + private static final String PLAYLIST_PLAYBACKLOG_ID = "_playbackLog"; + + private static final String PLAYLIST_FAVORITES_ID = "_favorites"; + + private static final String PLAYLIST_SOCIALACTIONS_ID = "_socialActions"; + + private static final String PLAYLIST_FRIENDSFEED_ID = "_friendsfeed"; + + private String mId; + + private String mName; + + private Image mImage; + + private String mAbout; + + private int mFollowCount = -1; + + private int mFollowersCount = -1; + + private Query mNowPlaying; + + private Date mNowPlayingTimeStamp; + + private int mTotalPlays; + + private final TreeMap> mSocialActions = new TreeMap<>(); + + private final TreeMap> mFriendsFeed = new TreeMap<>(); + + private Date mSocialActionsNextDate = new Date(); + + private Date mFriendsFeedNextDate = new Date(); + + private Playlist mSocialActionsPlaylist; + + private Playlist mFriendsFeedPlaylist; + + private Set mSocialActionsDoneConversions = new HashSet<>(); + + private Set mFriendsFeedDoneConversions = new HashSet<>(); + + private final Map mPlaylistEntryMap = new HashMap<>(); + + private Playlist mPlaybackLog; + + private Playlist mFavorites; + + private TreeMap mFollowings; + + private TreeMap mFollowers; + + private List mStarredAlbums = new ArrayList<>(); + + private List mStarredArtists = new ArrayList<>(); + + private List mPlaylists; + + private boolean mIsOffline; + + private Map mRelationships = new ConcurrentHashMap<>(); + + /** + * Construct a new {@link User} with the given id + */ + private User(String id) { + super(User.class, id); + + mId = id; + mPlaybackLog = Playlist.fromEmptyList(id + User.PLAYLIST_PLAYBACKLOG_ID, ""); + mFavorites = Playlist.fromEmptyList(id + User.PLAYLIST_FAVORITES_ID, ""); + mFavorites.setFilled(true); + mSocialActionsPlaylist = + Playlist.fromEmptyList(id + User.PLAYLIST_SOCIALACTIONS_ID, ""); + mFriendsFeedPlaylist = Playlist.fromEmptyList(id + User.PLAYLIST_FRIENDSFEED_ID, ""); + } + + /** + * Returns the {@link User} with the given id. If none exists in our static {@link + * ConcurrentHashMap} yet, construct and add it. + * + * @return {@link User} with the given id + */ + public static User get(String id) { + Cacheable cacheable = get(User.class, id); + return cacheable != null ? (User) cacheable : new User(id); + } + + public static User getUserById(String id) { + return (User) get(User.class, id); + } + + public static Promise getSelf() { + final ADeferredObject deferred = new ADeferredObject<>(); + final HatchetAuthenticatorUtils authUtils = (HatchetAuthenticatorUtils) AuthenticatorManager + .get().getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + authUtils.getUserId().done(new DoneCallback() { + @Override + public void onDone(String result) { + if (result != null && !mSelf.getId().equals(result)) { + mSelf.setName(authUtils.getUserName()); + mSelf.setId(result); + mSelf.setIsOffline(false); + } + deferred.resolve(mSelf); + } + }).fail(new FailCallback() { + @Override + public void onFail(Throwable result) { + deferred.resolve(mSelf); + } + }); + return deferred; + } + + public void putRelationship(Object object, Relationship relationship) { + mRelationships.put(object, relationship); + } + + public Relationship getRelationship(Object object) { + return mRelationships.get(object); + } + + public boolean isOffline() { + return mIsOffline; + } + + public void setIsOffline(boolean isOffline) { + mIsOffline = isOffline; + } + + private void setId(String id) { + mId = id; + put(User.class, id, this); + } + + /** + * @return this object' name + */ + public String getName() { + return mName; + } + + public void setName(final String name) { + mName = name; + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + String playbackLogName; + String favoritesName; + if (User.this == result) { + playbackLogName = TomahawkApp.getContext().getString(R.string.my_playbacklog); + favoritesName = TomahawkApp.getContext().getString(R.string.my_favorites); + } else { + playbackLogName = TomahawkApp.getContext().getString( + R.string.users_playbacklog_suffix, name); + favoritesName = TomahawkApp.getContext().getString( + R.string.users_favorites_suffix, name); + } + mPlaybackLog.setName(playbackLogName); + mFavorites.setName(favoritesName); + } + }); + } + + public Image getImage() { + return mImage; + } + + public void setImage(Image image) { + mImage = image; + } + + public String getId() { + return mId; + } + + public String getAbout() { + return mAbout; + } + + public void setAbout(String about) { + mAbout = about; + } + + public int getFollowCount() { + return mFollowCount; + } + + public void setFollowCount(int followCount) { + mFollowCount = followCount; + } + + public int getFollowersCount() { + return mFollowersCount; + } + + public void setFollowersCount(int followersCount) { + mFollowersCount = followersCount; + } + + public Query getNowPlaying() { + return mNowPlaying; + } + + public void setNowPlaying(Query nowPlaying) { + mNowPlaying = nowPlaying; + } + + public Date getNowPlayingTimeStamp() { + return mNowPlayingTimeStamp; + } + + public void setNowPlayingTimeStamp(Date nowPlayingTimeStamp) { + mNowPlayingTimeStamp = nowPlayingTimeStamp; + } + + public int getTotalPlays() { + return mTotalPlays; + } + + public void setTotalPlays(int totalPlays) { + mTotalPlays = totalPlays; + } + + public TreeMap> getSocialActions() { + return mSocialActions; + } + + public void setSocialActions(List socialActions, Date date) { + if (socialActions != null && socialActions.size() > 0) { + mSocialActions.put(date, socialActions); + SocialAction socialAction = socialActions.get(socialActions.size() - 1); + if (socialAction != null) { + if (socialAction.getDate().getTime() < date.getTime()) { + mSocialActionsNextDate = socialAction.getDate(); + } + } + fillPlaylist(mSocialActionsPlaylist, mSocialActions, mSocialActionsDoneConversions); + } + } + + public Date getSocialActionsNextDate() { + return mSocialActionsNextDate; + } + + public TreeMap> getFriendsFeed() { + return mFriendsFeed; + } + + public void setFriendsFeed(List friendsFeed, Date date) { + if (friendsFeed != null && friendsFeed.size() > 0) { + mFriendsFeed.put(date, friendsFeed); + SocialAction socialAction = friendsFeed.get(friendsFeed.size() - 1); + if (socialAction != null) { + if (socialAction.getDate().getTime() < date.getTime()) { + mFriendsFeedNextDate = socialAction.getDate(); + } + } + fillPlaylist(mFriendsFeedPlaylist, mFriendsFeed, mFriendsFeedDoneConversions); + } + } + + public Date getFriendsFeedNextDate() { + return mFriendsFeedNextDate; + } + + public Playlist getSocialActionsPlaylist() { + return mSocialActionsPlaylist; + } + + public Playlist getFriendsFeedPlaylist() { + return mFriendsFeedPlaylist; + } + + private void fillPlaylist(Playlist playlist, TreeMap> actions, + Set doneConversions) { + for (Date date : actions.keySet()) { + if (!doneConversions.contains(date)) { + doneConversions.add(date); + List> mergedActions = + SocialActionsFragment.mergeSocialActions(actions.get(date)); + for (List actionsList : mergedActions) { + for (SocialAction action : actionsList) { + if (action.getTargetObject() instanceof Query) { + Query query = (Query) action.getTargetObject(); + PlaylistEntry entry = playlist.addQuery(playlist.size(), query); + mPlaylistEntryMap.put(action, entry); + } + } + } + } + } + } + + public PlaylistEntry getPlaylistEntry(SocialAction item) { + return mPlaylistEntryMap.get(item); + } + + public Playlist getPlaybackLog() { + return mPlaybackLog; + } + + public void setPlaybackLog(Playlist playbackLog) { + if (playbackLog != null) { + mPlaybackLog = playbackLog; + } + } + + public Playlist getFavorites() { + return mFavorites; + } + + public void setFavorites(Playlist favorites) { + if (favorites != null) { + favorites.setUserId(mId); + favorites.setFilled(true); + mFavorites = favorites; + } + } + + public TreeMap getFollowings() { + return mFollowings; + } + + public void setFollowings(TreeMap followings) { + mFollowings = followings; + } + + public TreeMap getFollowers() { + return mFollowers; + } + + public void setFollowers(TreeMap followers) { + mFollowers = followers; + } + + public List getStarredAlbums() { + return mStarredAlbums; + } + + public void setStarredAlbums(List starredAlbums) { + mStarredAlbums = starredAlbums; + } + + public List getStarredArtists() { + return mStarredArtists; + } + + public void setStarredArtists( + List starredArtists) { + mStarredArtists = starredArtists; + } + + public List getPlaylists() { + return mPlaylists; + } + + public void setPlaylists(List playlists) { + if (playlists != null) { + for (Playlist playlist : playlists) { + playlist.setUserId(mId); + } + } + mPlaylists = playlists; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsCountryCodes.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsCountryCodes.java new file mode 100644 index 000000000..b2a1c3a52 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsCountryCodes.java @@ -0,0 +1,32 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.charts; + +import android.support.v4.util.Pair; + +import java.util.List; + +public class ScriptChartsCountryCodes { + + public String defaultCode; + + public List> codes; + + public ScriptChartsCountryCodes() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsManager.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsManager.java new file mode 100644 index 000000000..d493e4171 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsManager.java @@ -0,0 +1,66 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.charts; + +import java.util.HashMap; +import java.util.Map; + +import de.greenrobot.event.EventBus; + +public class ScriptChartsManager { + + public static final String TAG = ScriptChartsManager.class.getSimpleName(); + + private static class Holder { + + private static final ScriptChartsManager instance = new ScriptChartsManager(); + + } + + public class ProviderAddedEvent { + + } + + private Map mScriptChartsProviderMap = new HashMap<>(); + + private ScriptChartsManager() { + + } + + public static ScriptChartsManager get() { + return Holder.instance; + } + + public void addScriptChartsProvider(ScriptChartsProvider provider) { + mScriptChartsProviderMap.put(provider.getScriptAccount().getName(), provider); + EventBus.getDefault().post(new ProviderAddedEvent()); + } + + public void removeScriptChartsProvider(ScriptChartsProvider provider) { + mScriptChartsProviderMap.remove(provider.getScriptAccount().getName()); + } + + public Map getAllScriptChartsProvider() { + return mScriptChartsProviderMap; + } + + public ScriptChartsProvider getScriptChartsProvider(String chartsProviderId) { + return mScriptChartsProviderMap.get(chartsProviderId); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsProvider.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsProvider.java new file mode 100644 index 000000000..1ea449002 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsProvider.java @@ -0,0 +1,156 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.charts; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.jdeferred.Deferred; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptJob; +import org.tomahawk.libtomahawk.resolver.ScriptObject; +import org.tomahawk.libtomahawk.resolver.ScriptPlugin; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.GsonHelper; + +import android.support.v4.util.Pair; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ScriptChartsProvider implements ScriptPlugin { + + public static final String TAG = ScriptChartsProvider.class.getSimpleName(); + + // cache the result for a maximum of 12 hours + private static final long CACHE_TIME = 43200000L; + + private ScriptAccount mScriptAccount; + + private ScriptObject mScriptObject; + + private Map> mCachedResults = new ConcurrentHashMap<>(); + + public ScriptChartsProvider(ScriptObject scriptObject, ScriptAccount account) { + mScriptObject = scriptObject; + mScriptAccount = account; + } + + public Promise getCountryCodes() { + final Deferred deferred = + new ADeferredObject<>(); + ScriptJob.start(mScriptObject, "countryCodes", new ScriptJob.ResultsObjectCallback() { + @Override + public void onReportResults(JsonObject results) { + ScriptChartsCountryCodes chartsCountryCodes = new ScriptChartsCountryCodes(); + chartsCountryCodes.defaultCode = results.get("defaultCode").getAsString(); + List> codes = new ArrayList<>(); + JsonArray rawCodes = results.getAsJsonArray("codes"); + for (JsonElement result : rawCodes) { + JsonObject code = (JsonObject) result; + for (Map.Entry member : code.entrySet()) { + Pair pair = + new Pair<>(member.getKey(), member.getValue().getAsString()); + codes.add(pair); + } + } + chartsCountryCodes.codes = codes; + deferred.resolve(chartsCountryCodes); + } + }); + return deferred; + } + + public Promise>, Throwable, Void> getTypes() { + final Deferred>, Throwable, Void> deferred + = new ADeferredObject<>(); + ScriptJob.start(mScriptObject, "types", new ScriptJob.ResultsArrayCallback() { + @Override + public void onReportResults(JsonArray results) { + List> types = new ArrayList<>(); + for (JsonElement result : results) { + JsonObject type = (JsonObject) result; + for (Map.Entry member : type.entrySet()) { + Pair pair = + new Pair<>(member.getKey(), member.getValue().getAsString()); + types.add(pair); + } + } + deferred.resolve(types); + } + }); + return deferred; + } + + public Promise getCharts(final String countryCode, + final String type) { + final Deferred deferred = new ADeferredObject<>(); + + final String cacheKey = getCacheKey(countryCode, type); + final Pair pair = mCachedResults.get(cacheKey); + if (pair != null && pair.first > System.currentTimeMillis() - CACHE_TIME) { + Log.d(TAG, "Using cached charts for " + mScriptAccount.getName() + + ": countryCode=" + countryCode + ", type=" + type + + " - containing " + pair.second.results.size() + " results"); + deferred.resolve(pair.second); + } else { + Log.d(TAG, "Getting fresh charts for " + mScriptAccount.getName() + ": countryCode=" + + countryCode + ", type=" + type); + Map args = new HashMap<>(); + args.put("countryCode", countryCode); + args.put("type", type); + + ScriptJob.start(mScriptObject, "charts", args, new ScriptJob.ResultsObjectCallback() { + @Override + public void onReportResults(JsonObject results) { + ScriptChartsResult result = + GsonHelper.get().fromJson(results, ScriptChartsResult.class); + Pair pair = + new Pair<>(System.currentTimeMillis(), result); + mCachedResults.put(cacheKey, pair); + Log.d(TAG, "Received fresh charts for " + mScriptAccount.getName() + + ": countryCode=" + countryCode + ", type=" + type + + " - containing " + result.results.size() + " results"); + deferred.resolve(result); + } + }); + } + return deferred; + } + + private String getCacheKey(String countryCode, String type) { + return countryCode + "\t\t" + type; + } + + @Override + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + @Override + public ScriptObject getScriptObject() { + return mScriptObject; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsResult.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsResult.java new file mode 100644 index 000000000..0680ff714 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/charts/ScriptChartsResult.java @@ -0,0 +1,33 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.charts; + +import com.google.gson.JsonObject; + +import java.util.List; + +public class ScriptChartsResult { + + public int contentType; + + public List results; + + public ScriptChartsResult() { + + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Chart.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Chart.java new file mode 100644 index 000000000..443a821a2 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Chart.java @@ -0,0 +1,33 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import java.util.List; + +public class Chart { + + private List mChartItems; + + public Chart(List chartItems) { + mChartItems = chartItems; + } + + public List getChartItems() { + return mChartItems; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/ChartItem.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/ChartItem.java new file mode 100644 index 000000000..2e38f26e2 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/ChartItem.java @@ -0,0 +1,47 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import org.tomahawk.libtomahawk.resolver.Query; + +public class ChartItem { + + private Query mQuery; + + private int mPlays; + + private int mListeners; + + public ChartItem(Query query, int plays, int listeners) { + mQuery = query; + mPlays = plays; + mListeners = listeners; + } + + public Query getQuery() { + return mQuery; + } + + public int getPlays() { + return mPlays; + } + + public int getListeners() { + return mListeners; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Hatchet.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Hatchet.java new file mode 100644 index 000000000..79979503a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Hatchet.java @@ -0,0 +1,158 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import com.google.gson.JsonObject; + +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistEntries; + +import java.util.List; + +import retrofit.client.Response; +import retrofit.http.Body; +import retrofit.http.DELETE; +import retrofit.http.GET; +import retrofit.http.Header; +import retrofit.http.POST; +import retrofit.http.PUT; +import retrofit.http.Path; +import retrofit.http.Query; +import retrofit.mime.TypedInput; + +public interface Hatchet { + + @GET("/users") + JsonObject getUsers( + @Query("ids[]") List ids, + @Query("name") String name, + @Query("random") String random, + @Query("count") String count + ); + + @GET("/playlists") + JsonObject getPlaylists( + @Query("ids[]") List ids + ); + + @GET("/playlists/{id}") + JsonObject getPlaylists( + @Path("id") String id + ); + + @GET("/artists") + JsonObject getArtists( + @Query("ids[]") List ids, + @Query("name") String name + ); + + @GET("/tracks") + JsonObject getTracks( + @Query("ids[]") List ids, + @Query("name") String name, + @Query("artist_name") String artist_name + ); + + @GET("/albums") + JsonObject getAlbums( + @Query("ids[]") List ids, + @Query("name") String name, + @Query("artist_name") String artist_name + ); + + @GET("/searches") + JsonObject getSearches( + @Query("term") String term + ); + + @GET("/relationships") + JsonObject getRelationships( + @Query("ids[]") List ids, + @Query("user_id") String user_id, + @Query("target_type") String target_type, + @Query("target_user_id") String target_user_id, + @Query("target_artist_id") String target_artist_id, + @Query("target_album_id") String target_album_id, + @Query("filter") String filter, + @Query("type") String type + ); + + @GET("/socialActions") + JsonObject getSocialActions( + @Query("ids[]") List ids, + @Query("user_id") String user_id, + @Query("type") String type, + @Query("before_date") String before_date, + @Query("limit") String limit + ); + + @GET("/images") + JsonObject getImages( + @Query("ids[]") List ids + ); + + @POST("/playbacklogEntries") + Response postPlaybackLogEntries( + @Header("Authorization") String accesstoken, + @Body TypedInput rawBody + ); + + @POST("/playlists") + HatchetPlaylistEntries postPlaylists( + @Header("Authorization") String accesstoken, + @Body TypedInput rawBody + ); + + @POST("/playlistEntries") + HatchetPlaylistEntries postPlaylistsPlaylistEntries( + @Header("Authorization") String accesstoken, + @Body TypedInput rawBody + ); + + @POST("/relationships") + HatchetPlaylistEntries postRelationship( + @Header("Authorization") String accesstoken, + @Body TypedInput rawBody + ); + + @PUT("/playlists/{playlist-id}") + Response putPlaylists( + @Header("Authorization") String accesstoken, + @Path("playlist-id") String playlist_id, + @Body TypedInput rawBody + ); + + @DELETE("/playlists/{playlist-id}") + Response deletePlaylists( + @Header("Authorization") String accesstoken, + @Path("playlist-id") String playlist_id + ); + + @DELETE("/playlistEntries/{entry-id}") + Response deletePlaylistsPlaylistEntries( + @Header("Authorization") String accesstoken, + @Path("entry-id") String entry_id, + @Query("playlist_id") String playlist_id + ); + + @DELETE("/relationships/{relationship-id}") + Response deleteRelationShip( + @Header("Authorization") String accesstoken, + @Path("relationship-id") String relationship_id + ); + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/HatchetInfoPlugin.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/HatchetInfoPlugin.java new file mode 100644 index 000000000..6d3146632 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/HatchetInfoPlugin.java @@ -0,0 +1,343 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import com.google.gson.JsonObject; + +import org.apache.commons.io.Charsets; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.HatchetCollection; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.infosystem.InfoPlugin; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.QueryParams; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.infosystem.hatchet.models.HatchetPlaylistEntries; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ISO8601Utils; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import retrofit.RetrofitError; +import retrofit.mime.TypedByteArray; + +/** + * Implementation to enable the InfoSystem to retrieve data from the Hatchet API. Documentation of + * the API can be found here https://api.hatchet.is/apidocs/ + */ +public class HatchetInfoPlugin implements InfoPlugin { + + private final static String TAG = HatchetInfoPlugin.class.getSimpleName(); + + public static final String HATCHET_SEARCHITEM_TYPE_ALBUM = "album"; + + public static final String HATCHET_SEARCHITEM_TYPE_ARTIST = "artist"; + + public static final String HATCHET_SEARCHITEM_TYPE_USER = "user"; + + public static final String HATCHET_SOCIALACTION_PARAMTYPE_FRIENDSFEED = "friendsFeed"; + + public static final String HATCHET_SOCIALACTION_TYPE_LOVE = "love"; + + public static final String HATCHET_SOCIALACTION_TYPE_FOLLOW = "follow"; + + public static final String HATCHET_SOCIALACTION_TYPE_CREATECOMMENT = "createcomment"; + + public static final String HATCHET_SOCIALACTION_TYPE_LATCHON = "latchOn"; + + public static final String HATCHET_SOCIALACTION_TYPE_LATCHOFF = "latchOff"; + + public static final String HATCHET_SOCIALACTION_TYPE_CREATEPLAYLIST = "createplaylist"; + + public static final String HATCHET_SOCIALACTION_TYPE_DELETEPLAYLIST = "deleteplaylist"; + + public static final String HATCHET_RELATIONSHIPS_TYPE_FOLLOW = "follow"; + + public static final String HATCHET_RELATIONSHIPS_TYPE_LOVE = "love"; + + public static final String HATCHET_RELATIONSHIPS_TARGETTYPE_ALBUM = "album"; + + public static final String HATCHET_RELATIONSHIPS_TARGETTYPE_ARTIST = "artist"; + + public static final int SOCIALACTIONS_LIMIT = 20; + + public static final int FRIENDSFEED_LIMIT = 50; + + private HatchetAuthenticatorUtils mHatchetAuthenticatorUtils; + + private final Store mStore; + + public HatchetInfoPlugin() { + mStore = new Store(); + } + + /** + * _fetch_ data from the Hatchet API (e.g. artist's top-hits, image etc.) + */ + public void resolve(final InfoRequestData infoRequestData) { + int priority; + if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS_TOPHITSANDALBUMS) { + priority = TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_HIGH; + } else if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS + || infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDITEMS) { + priority = TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_LOW; + } else { + priority = TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_MEDIUM; + } + TomahawkRunnable runnable = new TomahawkRunnable(priority) { + @Override + public void run() { + try { + boolean success = getParseConvert(infoRequestData); + InfoSystem.get().reportResults(infoRequestData, success); + } catch (IOException e) { + Log.e(TAG, "resolve: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + }; + ThreadManager.get().execute(runnable); + } + + /** + * Core method of this InfoPlugin. Gets and parses the ordered results. + * + * @param infoRequestData InfoRequestData object containing the input parameters. + * @return true if the type of the given InfoRequestData was valid and could be processed. false + * otherwise + */ + private boolean getParseConvert(InfoRequestData infoRequestData) throws IOException { + QueryParams params = infoRequestData.getQueryParams(); + HatchetCollection hatchetCollection = CollectionManager.get().getHatchetCollection(); + Hatchet hatchet = mStore.getImplementation(infoRequestData.isBackgroundRequest()); + + try { + int type = infoRequestData.getType(); + if (type >= InfoRequestData.INFOREQUESTDATA_TYPE_USERS + && type < InfoRequestData.INFOREQUESTDATA_TYPE_USERS + 100) { + JsonObject object = + hatchet.getUsers(params.ids, params.name, params.random, params.count); + if (object == null) { + return false; + } + List users = mStore.storeRecords(object, User.class, type, + infoRequestData.isBackgroundRequest()); + infoRequestData.setResultList(users); + return true; + + } else if (type >= InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS + && type < InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS + 100) { + JsonObject object = hatchet.getPlaylists(params.playlist_id); + if (object == null) { + return false; + } + List playlists = mStore.storeRecords(object, Playlist.class, type, + infoRequestData.isBackgroundRequest()); + infoRequestData.setResultList(playlists); + return true; + + } else if (type >= InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS + && type < InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS + 100) { + JsonObject object = hatchet.getArtists(params.ids, params.name); + if (object == null) { + return false; + } + if (type == InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS_TOPHITSANDALBUMS) { + List topHits = mStore.storeRecords(object, Query.class, type, + infoRequestData.isBackgroundRequest()); + Artist artist = Artist.get(params.name); + Playlist playlist = Playlist.fromQueryList(TomahawkApp.PLUGINNAME_HATCHET + "_" + + artist.getCacheKey(), null, null, topHits); + hatchetCollection.addArtistTopHits(artist, playlist); + + List albums = mStore.storeRecords(object, Album.class, type, + infoRequestData.isBackgroundRequest()); + if (albums.size() > 0) { + for (Album album : albums) { + hatchetCollection.addAlbum(album); + } + Album firstAlbum = albums.get(0); + hatchetCollection.addArtistAlbums(firstAlbum.getArtist(), albums); + } + } + List artists = mStore.storeRecords(object, Artist.class, type, + infoRequestData.isBackgroundRequest()); + for (Artist artist : artists) { + hatchetCollection.addArtist(artist); + } + infoRequestData.setResultList(artists); + return true; + + } else if (type >= InfoRequestData.INFOREQUESTDATA_TYPE_ALBUMS + && type < InfoRequestData.INFOREQUESTDATA_TYPE_ALBUMS + 100) { + JsonObject object = hatchet.getAlbums(params.ids, params.name, params.artistname); + if (object == null) { + return false; + } + if (type == InfoRequestData.INFOREQUESTDATA_TYPE_ALBUMS_TRACKS) { + List tracks = mStore.storeRecords(object, Query.class, type, + infoRequestData.isBackgroundRequest()); + Artist artist = Artist.get(params.artistname); + Album album = Album.get(params.name, artist); + Playlist playlist = Playlist.fromQueryList(TomahawkApp.PLUGINNAME_HATCHET + "_" + + album.getCacheKey(), null, null, tracks); + playlist.setFilled(true); + hatchetCollection.addAlbumTracks(album, playlist); + } + List albums = mStore.storeRecords(object, Album.class, type, + infoRequestData.isBackgroundRequest()); + for (Album album : albums) { + hatchetCollection.addAlbum(album); + } + infoRequestData.setResultList(albums); + return true; + + } else if (type == InfoRequestData.INFOREQUESTDATA_TYPE_SEARCHES) { + JsonObject object = hatchet.getSearches(params.term); + if (object == null) { + return false; + } + List searches = mStore.storeRecords(object, Search.class, type, + infoRequestData.isBackgroundRequest()); + infoRequestData.setResultList(searches); + return true; + + } else if (type == InfoRequestData.INFOREQUESTDATA_TYPE_SOCIALACTIONS) { + JsonObject object = hatchet.getSocialActions(null, params.userid, params.type, + ISO8601Utils.format(params.before_date), params.limit); + if (object == null) { + return false; + } + List socialActions = mStore.storeRecords(object, SocialAction.class, + type, infoRequestData.isBackgroundRequest(), params); + infoRequestData.setResultList(socialActions); + return true; + } + } catch (RetrofitError e) { + Log.e(TAG, "getParseConvert: Request to " + e.getUrl() + " failed: " + e.getClass() + + ": " + e.getLocalizedMessage()); + } + return false; + } + + /** + * Start the JSONSendTask to send the given InfoRequestData's json string + */ + @Override + public void send(final InfoRequestData infoRequestData, AuthenticatorUtils authenticatorUtils) { + mHatchetAuthenticatorUtils = (HatchetAuthenticatorUtils) authenticatorUtils; + TomahawkRunnable runnable = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_MEDIUM) { + @Override + public void run() { + ArrayList doneRequestsIds = new ArrayList<>(); + doneRequestsIds.add(infoRequestData.getRequestId()); + Hatchet hatchet = mStore.getImplementation(infoRequestData.isBackgroundRequest()); + // Before we do anything, get the accesstoken + boolean success = false; + boolean discard = false; + String accessToken = mHatchetAuthenticatorUtils.ensureAccessTokens(); + if (accessToken != null) { + String data = infoRequestData.getJsonStringToSend(); + try { + if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYBACKLOGENTRIES) { + hatchet.postPlaybackLogEntries(accessToken, + new TypedByteArray("application/json; charset=utf-8", + data.getBytes(Charsets.UTF_8))); + } else if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS) { + if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_POST) { + HatchetPlaylistEntries entries = hatchet.postPlaylists(accessToken, + new TypedByteArray("application/json; charset=utf-8", + data.getBytes(Charsets.UTF_8))); + List results = new ArrayList<>(); + results.add(entries); + infoRequestData.setResultList(results); + } else if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_DELETE) { + hatchet.deletePlaylists(accessToken, + infoRequestData.getQueryParams().playlist_id); + } else if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_PUT) { + hatchet.putPlaylists(accessToken, + infoRequestData.getQueryParams().playlist_id, + new TypedByteArray("application/json; charset=utf-8", + data.getBytes(Charsets.UTF_8))); + } + } else if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_POST) { + hatchet.postPlaylistsPlaylistEntries(accessToken, + new TypedByteArray("application/json; charset=utf-8", + data.getBytes(Charsets.UTF_8))); + } else if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_DELETE) { + hatchet.deletePlaylistsPlaylistEntries(accessToken, + infoRequestData.getQueryParams().entry_id, + infoRequestData.getQueryParams().playlist_id); + } + } else if (infoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_RELATIONSHIPS) { + if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_POST) { + hatchet.postRelationship(accessToken, + new TypedByteArray("application/json; charset=utf-8", + data.getBytes(Charsets.UTF_8))); + } else if (infoRequestData.getHttpType() + == InfoRequestData.HTTPTYPE_DELETE) { + hatchet.deleteRelationShip(accessToken, + infoRequestData.getQueryParams().relationship_id); + } + } + success = true; + discard = true; + } catch (RetrofitError e) { + Log.e(TAG, "send: Request to " + e.getUrl() + " failed: " + e.getClass() + + ": " + e.getLocalizedMessage()); + if (e.getResponse() != null && e.getResponse().getStatus() == 500) { + Log.e(TAG, "send: discarding oplog that has failed to be sent to " + e + .getUrl()); + discard = true; + } + } + } + InfoSystem.get().onLoggedOpsSent(doneRequestsIds, discard); + InfoSystem.get().reportResults(infoRequestData, success); + } + }; + ThreadManager.get().execute(runnable); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/PlaybackLogEntry.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/PlaybackLogEntry.java new file mode 100644 index 000000000..72479823d --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/PlaybackLogEntry.java @@ -0,0 +1,42 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import org.tomahawk.libtomahawk.resolver.Query; + +import java.util.Date; + +public class PlaybackLogEntry { + + private Query mQuery; + + private Date mTimeStamp; + + public PlaybackLogEntry(Query query, Date timeStamp) { + mQuery = query; + mTimeStamp = timeStamp; + } + + public Query getQuery() { + return mQuery; + } + + public Date getTimeStamp() { + return mTimeStamp; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Search.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Search.java new file mode 100644 index 000000000..4a8cb9bfe --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Search.java @@ -0,0 +1,33 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import java.util.List; + +public class Search { + + private List mSearchResults; + + public Search(List searchResults) { + mSearchResults = searchResults; + } + + public List getSearchResults() { + return mSearchResults; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/SearchResult.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/SearchResult.java new file mode 100644 index 000000000..70c53be75 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/SearchResult.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +public class SearchResult { + + private float mScore; + + private Object mResult; + + public SearchResult(float score, Object result) { + this.mScore = score; + this.mResult = result; + } + + public float getScore() { + return mScore; + } + + public Object getResult() { + return mResult; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Store.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Store.java new file mode 100644 index 000000000..b5b25a0c5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/Store.java @@ -0,0 +1,870 @@ +/* == This file is part of Tomahawk Player - === +* +* Copyright 2015, Enno Gottschalk +* +* Tomahawk is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Tomahawk is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Tomahawk. If not, see . +*/ +package org.tomahawk.libtomahawk.infosystem.hatchet; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.AlphaComparator; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.ListItemString; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistComparator; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.QueryParams; +import org.tomahawk.libtomahawk.infosystem.Relationship; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.libtomahawk.utils.ISO8601Utils; +import org.tomahawk.libtomahawk.utils.NetworkUtils; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import retrofit.RequestInterceptor; +import retrofit.RestAdapter; +import retrofit.android.MainThreadExecutor; +import retrofit.client.OkClient; +import retrofit.converter.GsonConverter; + +import static android.os.Process.THREAD_PRIORITY_LOWEST; + +public class Store { + + private final static String TAG = Store.class.getSimpleName(); + + public static final String HATCHET_BASE_URL = "https://api.hatchet.is"; + + public static final String HATCHET_API_VERSION = "/v2"; + + private final Cache mCache = new Cache(); + + private static class Cache { + + private Map mCaches = new HashMap<>(); + + public Cache() { + } + + public void addCache(Class clss) { + mCaches.put(clss, new HashMap()); + } + + public void put(Class clss, String id, T object) { + mCaches.get(clss).put(id, object); + } + + public T get(Class clss, String id) { + return (T) mCaches.get(clss).get(id); + } + + } + + private final OkHttpClient mOkHttpClient; + + private final Hatchet mHatchet; + + private final Hatchet mHatchetBackground; + + public Store() { + RequestInterceptor requestInterceptor = new RequestInterceptor() { + @Override + public void intercept(RequestFacade request) { + if (!NetworkUtils.isNetworkAvailable()) { + int maxStale = 60 * 60 * 24 * 7; // tolerate 1-week stale + request.addHeader("Cache-Control", "public, max-stale=" + maxStale); + } + request.addHeader("Content-type", "application/json; charset=utf-8"); + } + }; + mOkHttpClient = new OkHttpClient(); + File cacheDir = new File(TomahawkApp.getContext().getCacheDir(), "responseCache"); + com.squareup.okhttp.Cache cache = new com.squareup.okhttp.Cache(cacheDir, 1024 * 1024 * 20); + mOkHttpClient.setCache(cache); + RestAdapter restAdapter = new RestAdapter.Builder() + .setLogLevel(RestAdapter.LogLevel.BASIC) + .setEndpoint(HATCHET_BASE_URL + HATCHET_API_VERSION) + .setConverter(new GsonConverter(GsonHelper.get())) + .setRequestInterceptor(requestInterceptor) + .setClient(new OkClient(mOkHttpClient)) + .build(); + mHatchet = restAdapter.create(Hatchet.class); + + Executor httpExecutor = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + return new Thread(new Runnable() { + @Override + public void run() { + android.os.Process.setThreadPriority(THREAD_PRIORITY_LOWEST); + r.run(); + } + }, "Retrofit-Idle-Background"); + } + }); + restAdapter = new RestAdapter.Builder() + .setLogLevel(RestAdapter.LogLevel.BASIC) + .setEndpoint(HATCHET_BASE_URL + HATCHET_API_VERSION) + .setConverter(new GsonConverter(GsonHelper.get())) + .setRequestInterceptor(requestInterceptor) + .setClient(new OkClient(mOkHttpClient)) + .setExecutors(httpExecutor, new MainThreadExecutor()) + .build(); + mHatchetBackground = restAdapter.create(Hatchet.class); + + mCache.addCache(Image.class); + mCache.addCache(Artist.class); + mCache.addCache(Album.class); + mCache.addCache(Query.class); + mCache.addCache(ChartItem.class); + mCache.addCache(Chart.class); + mCache.addCache(PlaybackLogEntry.class); + mCache.addCache(PlaylistEntry.class); + mCache.addCache(User.class); + mCache.addCache(Playlist.class); + mCache.addCache(SocialAction.class); + mCache.addCache(Search.class); + mCache.addCache(SearchResult.class); + mCache.addCache(Relationship.class); + } + + public Hatchet getImplementation(boolean isBackgroundRequest) { + return isBackgroundRequest ? mHatchetBackground : mHatchet; + } + + public T findRecord(String id, Class resultType, boolean isBackgroundRequest) + throws IOException { + T record = mCache.get(resultType, id); + if (record == null) { + Hatchet hatchet = getImplementation(isBackgroundRequest); + List ids = new ArrayList<>(); + ids.add(String.valueOf(id)); + if (resultType == Image.class) { + storeRecords(hatchet.getImages(ids), resultType, isBackgroundRequest); + } else if (resultType == Artist.class) { + storeRecords(hatchet.getArtists(ids, null), resultType, isBackgroundRequest); + } else if (resultType == Album.class) { + storeRecords(hatchet.getAlbums(ids, null, null), resultType, isBackgroundRequest); + } else if (resultType == PlaylistEntry.class) { + storeRecords(hatchet.getTracks(ids, null, null), resultType, isBackgroundRequest); + } else if (resultType == User.class) { + storeRecords(hatchet.getUsers(ids, null, null, null), resultType, + isBackgroundRequest); + } else if (resultType == Playlist.class) { + storeRecords(hatchet.getPlaylists(ids), resultType, isBackgroundRequest); + } + record = mCache.get(resultType, id); + if (record == null) { + throw new IOException("Couldn't fetch entity from server."); + } + mCache.put(resultType, id, record); + } + return record; + } + + public List storeRecords(JsonObject object, Class resultType, + boolean isBackgroundRequest) + throws IOException { + return storeRecords(object, resultType, -1, isBackgroundRequest); + } + + public List storeRecords(JsonObject object, Class resultType, int requestType, + boolean isBackgroundRequest) + throws IOException { + return storeRecords(object, resultType, requestType, isBackgroundRequest, null); + } + + public List storeRecords(JsonObject object, Class resultType, int requestType, + boolean isBackgroundRequest, QueryParams params) + throws IOException { + List results = new ArrayList<>(); + JsonElement elements = object.get("images"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + Image image = mCache.get(Image.class, id); + if (image == null) { + String url = getAsString(o, "url"); + int width = getAsInt(o, "width"); + int height = getAsInt(o, "height"); + image = Image.get(url, true, width, height); + mCache.put(Image.class, id, image); + } + if (resultType == Image.class) { + results.add((T) image); + } + } + } + } + elements = object.get("artists"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + Artist artist = mCache.get(Artist.class, id); + if (artist == null) { + String name = getAsString(o, "name"); + String wiki = getAsString(o, "wikiabstract"); + artist = Artist.get(name); + artist.setBio(new ListItemString(wiki)); + JsonElement images = get(o, "images"); + if (images instanceof JsonArray && ((JsonArray) images).size() > 0) { + String imageId = ((JsonArray) images).get(0).getAsString(); + Image image = findRecord(imageId, Image.class, isBackgroundRequest); + artist.setImage(image); + } + mCache.put(Artist.class, id, artist); + } + + if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_ARTISTS_TOPHITSANDALBUMS) { + JsonElement rawAlbums = get(o, "albums"); + if (rawAlbums instanceof JsonObject && resultType == Album.class) { + results.addAll(storeRecords( + (JsonObject) rawAlbums, resultType, isBackgroundRequest)); + } + + JsonElement rawTopHits = get(o, "topHits"); + if (rawTopHits instanceof JsonObject && resultType == Query.class) { + List chartItems = storeRecords((JsonObject) rawTopHits, + Chart.class, isBackgroundRequest); + List topHits = new ArrayList<>(); + if (chartItems != null && chartItems.size() > 0) { + for (ChartItem item : chartItems.get(0).getChartItems()) { + topHits.add(item.getQuery()); + } + } + results.addAll((List) topHits); + } + } + if (resultType == Artist.class) { + results.add((T) artist); + } + } + } + } + elements = object.get("albums"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + Album album = mCache.get(Album.class, id); + if (album == null) { + String name = getAsString(o, "name"); + String artistId = getAsString(o, "artist"); + Artist artist = findRecord(artistId, Artist.class, isBackgroundRequest); + album = Album.get(name, artist); + JsonElement images = get(o, "images"); + if (images instanceof JsonArray && ((JsonArray) images).size() > 0) { + String imageId = ((JsonArray) images).get(0).getAsString(); + Image image = findRecord(imageId, Image.class, isBackgroundRequest); + album.setImage(image); + } + String releaseType = getAsString(o, "releaseType"); + album.setReleaseType(releaseType); + mCache.put(Album.class, id, album); + } + + if (requestType == InfoRequestData.INFOREQUESTDATA_TYPE_ALBUMS_TRACKS) { + JsonElement rawTracks = get(o, "tracks"); + if (rawTracks instanceof JsonObject && resultType == Query.class) { + results.addAll(storeRecords((JsonObject) rawTracks, resultType, + isBackgroundRequest)); + } + } + if (resultType == Album.class) { + results.add((T) album); + } + } + } + } + elements = object.get("tracks"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + Query query = mCache.get(Query.class, id); + if (query == null) { + String name = getAsString(o, "name"); + String artistId = getAsString(o, "artist"); + Artist artist = findRecord(artistId, Artist.class, isBackgroundRequest); + query = Query.get(name, null, artist.getName(), false, true); + mCache.put(Query.class, id, query); + } + if (resultType == Query.class) { + results.add((T) query); + } + } + } + } + elements = object.get("users"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + User user = User.get(id); + String name = getAsString(o, "name"); + user.setName(name); + String about = getAsString(o, "about"); + user.setAbout(about); + int followersCount = getAsInt(o, "followersCount"); + user.setFollowersCount(followersCount); + int followCount = getAsInt(o, "followCount"); + user.setFollowCount(followCount); + String nowplayingId = getAsString(o, "nowplaying"); + if (nowplayingId != null) { + Query nowplaying = + findRecord(nowplayingId, Query.class, isBackgroundRequest); + user.setNowPlaying(nowplaying); + } + String nowplayingtimestamp = getAsString(o, "nowplayingtimestamp"); + user.setNowPlayingTimeStamp(ISO8601Utils.parse(nowplayingtimestamp)); + String avatar = getAsString(o, "avatar"); + if (avatar != null) { + Image image = findRecord(avatar, Image.class, isBackgroundRequest); + user.setImage(image); + } + + if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_PLAYLISTS) { + JsonElement rawPlaylists = get(o, "playlists"); + if (rawPlaylists instanceof JsonObject) { + List playlists = storeRecords( + (JsonObject) rawPlaylists, Playlist.class, isBackgroundRequest); + Collections.sort(playlists, new PlaylistComparator()); + user.setPlaylists(playlists); + } + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDITEMS) { + JsonElement rawLovedItems = get(o, "lovedItems"); + if (rawLovedItems instanceof JsonObject) { + List playlists = storeRecords((JsonObject) rawLovedItems, + Playlist.class, isBackgroundRequest); + if (playlists != null && playlists.size() > 0) { + user.setFavorites(playlists.get(0)); + } + } + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDALBUMS) { + JsonElement rawLovedAlbums = get(o, "lovedAlbums"); + if (rawLovedAlbums instanceof JsonObject) { + List relationships = storeRecords( + (JsonObject) rawLovedAlbums, Relationship.class, + isBackgroundRequest); + List albums = new ArrayList<>(); + for (Relationship relationship : relationships) { + albums.add(relationship.getAlbum()); + } + user.setStarredAlbums(albums); + } + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_LOVEDARTISTS) { + JsonElement rawLovedArtists = get(o, "lovedArtists"); + if (rawLovedArtists instanceof JsonObject) { + List relationships = storeRecords( + (JsonObject) rawLovedArtists, Relationship.class, + isBackgroundRequest); + List artists = new ArrayList<>(); + for (Relationship relationship : relationships) { + artists.add(relationship.getArtist()); + } + user.setStarredArtists(artists); + } + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_PLAYBACKLOG) { + JsonElement rawPlaybackLog = get(o, "playbacklog"); + if (rawPlaybackLog instanceof JsonObject) { + List playlists = storeRecords((JsonObject) rawPlaybackLog, + Playlist.class, isBackgroundRequest); + if (playlists != null && playlists.size() > 0) { + user.setPlaybackLog(playlists.get(0)); + } + } + } else if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWS + || requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWERS) { + boolean isFollows = + requestType == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWS; + JsonElement rawFollows = get(o, isFollows ? "follows" : "followers"); + if (rawFollows instanceof JsonObject) { + JsonObject follows = (JsonObject) rawFollows; + storeRecords(follows, null, isBackgroundRequest); + JsonElement relationships = get(follows, "relationships"); + if (relationships instanceof JsonArray) { + TreeMap followsMap = + new TreeMap<>(new AlphaComparator()); + for (JsonElement relationship : (JsonArray) relationships) { + JsonObject relationshipObj = (JsonObject) relationship; + String relationshipId = getAsString(relationshipObj, "id"); + String userId = getAsString(relationshipObj, + isFollows ? "targetUser" : "user"); + User followedUser = + findRecord(userId, User.class, isBackgroundRequest); + followsMap.put(followedUser, relationshipId); + } + if (isFollows) { + user.setFollowings(followsMap); + } else { + user.setFollowers(followsMap); + } + } + } + } + mCache.put(User.class, id, user); + if (resultType == User.class) { + results.add((T) user); + } + } + } + } + elements = object.get("playlistEntries"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + PlaylistEntry entry = mCache.get(PlaylistEntry.class, id); + if (entry == null) { + String trackId = getAsString(o, "track"); + Query query = findRecord(trackId, Query.class, isBackgroundRequest); + String playlistId = getAsString(o, "playlist"); + entry = PlaylistEntry.get(playlistId, query, id); + mCache.put(PlaylistEntry.class, id, entry); + } + if (resultType == PlaylistEntry.class) { + results.add((T) entry); + } + } + } + } + elements = object.get("playlists"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + String title = getAsString(o, "title"); + String currentrevision = getAsString(o, "currentrevision"); + Playlist playlist = null; + if (requestType + == InfoRequestData.INFOREQUESTDATA_TYPE_PLAYLISTS_PLAYLISTENTRIES) { + JsonElement rawEntries = get(o, "playlistEntries"); + if (rawEntries instanceof JsonObject) { + List entries = storeRecords((JsonObject) rawEntries, + PlaylistEntry.class, isBackgroundRequest); + if (entries != null) { + playlist = Playlist.fromEntryList(id, null, null, entries); + playlist.setFilled(true); + } + } + } else { + JsonElement entryIds = o.get("playlistEntries"); + if (entryIds instanceof JsonArray) { + List entries = new ArrayList<>(); + for (JsonElement entryId : (JsonArray) entryIds) { + PlaylistEntry entry = findRecord(entryId.getAsString(), + PlaylistEntry.class, isBackgroundRequest); + entries.add(entry); + } + playlist = Playlist.fromEntryList(id, null, null, entries); + playlist.setFilled(true); + } + } + if (playlist == null) { + playlist = Playlist.get(id); + } + playlist.setName(title); + playlist.setCurrentRevision(currentrevision); + playlist.setHatchetId(id); + int entryCount = getAsInt(o, "entryCount"); + playlist.setCount(entryCount); + String userId = getAsString(o, "user"); + playlist.setUserId(userId); + JsonElement popularArtists = get(o, "popularArtists"); + if (popularArtists instanceof JsonArray) { + ArrayList topArtistNames = new ArrayList<>(); + for (JsonElement popularArtist : (JsonArray) popularArtists) { + String artistId = popularArtist.getAsString(); + Artist artist = findRecord(artistId, Artist.class, isBackgroundRequest); + if (artist != null) { + topArtistNames.add(artist.getName()); + } + } + playlist.setTopArtistNames( + topArtistNames.toArray(new String[topArtistNames.size()])); + } + mCache.put(Playlist.class, id, playlist); + if (resultType == Playlist.class) { + results.add((T) playlist); + } + } + } + } + elements = object.get("playbacklogEntries"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + PlaybackLogEntry logEntry = mCache.get(PlaybackLogEntry.class, id); + if (logEntry == null) { + String trackId = getAsString(o, "track"); + Query query = findRecord(trackId, Query.class, isBackgroundRequest); + String timestamp = getAsString(o, "timestamp"); + Date date = ISO8601Utils.parse(timestamp); + logEntry = new PlaybackLogEntry(query, date); + mCache.put(PlaybackLogEntry.class, id, logEntry); + } + if (resultType == PlaybackLogEntry.class) { + results.add((T) logEntry); + } + } + } + } + elements = object.get("playbacklogs"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + JsonArray playbacklogEntries = get(o, "playbacklogEntries").getAsJsonArray(); + ArrayList entries = new ArrayList<>(); + for (JsonElement entry : playbacklogEntries) { + String entryId = entry.getAsString(); + PlaybackLogEntry logEntry = + findRecord(entryId, PlaybackLogEntry.class, isBackgroundRequest); + PlaylistEntry e = PlaylistEntry.get(id, logEntry.getQuery(), entryId); + entries.add(e); + } + Playlist playlist = Playlist.fromEntryList(id, "Playbacklog", null, entries); + playlist.setHatchetId(id); + playlist.setFilled(true); + mCache.put(Playlist.class, id, playlist); + if (resultType == Playlist.class) { + results.add((T) playlist); + } + } + } + } + elements = object.get("socialActions"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + SocialAction socialAction = mCache.get(SocialAction.class, id); + if (socialAction == null) { + socialAction = SocialAction.get(id); + String action = getAsString(o, "action"); + socialAction.setAction(action); + String date = getAsString(o, "date"); + socialAction.setDate(ISO8601Utils.parse(date)); + String actionType = getAsString(o, "type"); + socialAction.setType(actionType); + String trackId = getAsString(o, "track"); + if (trackId != null) { + Query query = findRecord(trackId, Query.class, isBackgroundRequest); + socialAction.setQuery(query); + } + String artistId = getAsString(o, "artist"); + if (artistId != null) { + Artist artist = findRecord(artistId, Artist.class, isBackgroundRequest); + socialAction.setArtist(artist); + } + String albumId = getAsString(o, "album"); + if (albumId != null) { + Album album = findRecord(albumId, Album.class, isBackgroundRequest); + socialAction.setAlbum(album); + } + String userId = getAsString(o, "user"); + if (userId != null) { + User user = findRecord(userId, User.class, isBackgroundRequest); + socialAction.setUser(user); + } + String targetId = getAsString(o, "target"); + if (targetId != null) { + User target = findRecord(targetId, User.class, isBackgroundRequest); + socialAction.setTarget(target); + } + String playlistId = getAsString(o, "playlist"); + if (playlistId != null) { + Playlist playlist = + findRecord(playlistId, Playlist.class, isBackgroundRequest); + socialAction.setPlaylist(playlist); + } + mCache.put(SocialAction.class, id, socialAction); + } + if (resultType == SocialAction.class) { + results.add((T) socialAction); + } + } + } + if (params != null) { + User user = findRecord(params.userid, User.class, false); + if (user != null && resultType == SocialAction.class) { + if (HatchetInfoPlugin.HATCHET_SOCIALACTION_PARAMTYPE_FRIENDSFEED + .equals(params.type)) { + user.setFriendsFeed((List) results, params.before_date); + } else { + user.setSocialActions((List) results, params.before_date); + } + } + } + } + elements = object.get("searchResults"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + SearchResult searchResult = mCache.get(SearchResult.class, id); + if (searchResult == null) { + float score = getAsFloat(o, "score"); + String trackId = getAsString(o, "track"); + if (trackId != null) { + Query query = findRecord(trackId, Query.class, isBackgroundRequest); + searchResult = new SearchResult(score, query); + } + String artistId = getAsString(o, "artist"); + if (artistId != null) { + Artist artist = findRecord(artistId, Artist.class, isBackgroundRequest); + searchResult = new SearchResult(score, artist); + } + String albumId = getAsString(o, "album"); + if (albumId != null) { + Album album = findRecord(albumId, Album.class, isBackgroundRequest); + searchResult = new SearchResult(score, album); + } + String userId = getAsString(o, "user"); + if (userId != null) { + User user = findRecord(userId, User.class, isBackgroundRequest); + searchResult = new SearchResult(score, user); + } + String playlistId = getAsString(o, "playlist"); + if (playlistId != null) { + Playlist playlist = + findRecord(playlistId, Playlist.class, isBackgroundRequest); + searchResult = new SearchResult(score, playlist); + } + if (searchResult == null) { + throw new IOException( + "searchResult contained no actual result object!"); + } + mCache.put(SearchResult.class, id, searchResult); + } + if (resultType == SearchResult.class) { + results.add((T) searchResult); + } + } + } + } + elements = object.get("searches"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + JsonArray rawSearchResults = get(o, "searchResults").getAsJsonArray(); + ArrayList searchResults = new ArrayList<>(); + for (JsonElement rawSearchResult : rawSearchResults) { + String resultId = rawSearchResult.getAsString(); + SearchResult searchResult = + findRecord(resultId, SearchResult.class, isBackgroundRequest); + searchResults.add(searchResult); + } + Search search = new Search(searchResults); + mCache.put(Search.class, id, search); + if (resultType == Search.class) { + results.add((T) search); + } + } + } + } + elements = object.get("relationships"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + String type = getAsString(o, "type"); + if (type.equals(HatchetInfoPlugin.HATCHET_RELATIONSHIPS_TYPE_LOVE)) { + String userId = getAsString(o, "user"); + User user = findRecord(userId, User.class, isBackgroundRequest); + String dateString = getAsString(o, "date"); + Date date = null; + if (dateString != null) { + date = ISO8601Utils.parse(dateString); + } + Relationship relationship = Relationship.get(id, type, user, date); + String targetId; + if ((targetId = getAsString(o, "targetTrack")) != null) { + Query query = findRecord(targetId, Query.class, isBackgroundRequest); + relationship.setQuery(query); + user.putRelationship(query, relationship); + } else if ((targetId = getAsString(o, "targetAlbum")) != null) { + Album album = findRecord(targetId, Album.class, isBackgroundRequest); + relationship.setAlbum(album); + user.putRelationship(album, relationship); + } else if ((targetId = getAsString(o, "targetArtist")) != null) { + Artist artist = + findRecord(targetId, Artist.class, isBackgroundRequest); + relationship.setArtist(artist); + user.putRelationship(artist, relationship); + } + mCache.put(Relationship.class, id, relationship); + if (resultType == Relationship.class) { + results.add((T) relationship); + } + } + } + } + } + elements = object.get("chartItems"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + ChartItem item = mCache.get(ChartItem.class, id); + if (item == null) { + String trackid = getAsString(o, "track"); + Query query = findRecord(trackid, Query.class, isBackgroundRequest); + int plays = getAsInt(o, "plays"); + int listeners = getAsInt(o, "listeners"); + item = new ChartItem(query, plays, listeners); + mCache.put(ChartItem.class, id, item); + } + if (resultType == ChartItem.class) { + results.add((T) item); + } + } + } + } + elements = object.get("chart"); + if (elements instanceof JsonArray) { + for (JsonElement element : (JsonArray) elements) { + if (element instanceof JsonObject) { + JsonObject o = (JsonObject) element; + String id = getAsString(o, "id"); + Chart chart = mCache.get(Chart.class, id); + if (chart == null) { + List items = new ArrayList<>(); + JsonElement chartItems = get(o, "chartItems"); + if (chartItems instanceof JsonArray) { + for (JsonElement chartItemid : (JsonArray) chartItems) { + String itemId = chartItemid.getAsString(); + ChartItem chartItem = + findRecord(itemId, ChartItem.class, isBackgroundRequest); + items.add(chartItem); + } + } + chart = new Chart(items); + mCache.put(Chart.class, id, chart); + } + if (resultType == Chart.class) { + results.add((T) chart); + } + } + } + } + return results; + } + + public int getAsInt(JsonObject object, String memberName) throws IOException { + JsonElement element = get(object, memberName); + if (element != null && element.isJsonPrimitive()) { + return element.getAsInt(); + } + return -1; + } + + public float getAsFloat(JsonObject object, String memberName) throws IOException { + JsonElement element = get(object, memberName); + if (element != null && element.isJsonPrimitive()) { + return element.getAsFloat(); + } + return -1; + } + + public String getAsString(JsonObject object, String memberName) throws IOException { + JsonElement element = get(object, memberName); + if (element != null && element.isJsonPrimitive()) { + return element.getAsString(); + } + return null; + } + + public JsonElement get(JsonObject object, String memberName) throws IOException { + JsonElement element = object.get(memberName); + if (element == null) { + JsonObject links = object.getAsJsonObject("links"); + if (links != null && links.has(memberName)) { + Request request = new Request.Builder() + .url(HATCHET_BASE_URL + links.get(memberName).getAsString()) + .build(); + Log.d(TAG, "following link: " + request.urlString()); + Response response = mOkHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new IOException("API request with URL '" + request.urlString() + + "' not successful. Code was " + response.code()); + } + try { + element = GsonHelper.get().fromJson( + response.body().charStream(), JsonElement.class); + } catch (JsonIOException | JsonSyntaxException e) { + throw new IOException(e); + } finally { + response.body().close(); + } + } + } + return element; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetAlbumInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetAlbumInfo.java new file mode 100644 index 000000000..550746513 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetAlbumInfo.java @@ -0,0 +1,54 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; +import java.util.Map; + +public class HatchetAlbumInfo extends Mappable { + + public String artist; + + public List images; + + public List labels; + + public int length; + + public Map links; + + public int loveCount; + + public String name; + + public List names; + + public List producers; + + public String releasedate; + + public List tracks; + + public String url; + + public String wikiabstract; + + public HatchetAlbumInfo() { + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetArtistInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetArtistInfo.java new file mode 100644 index 000000000..a5506a575 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetArtistInfo.java @@ -0,0 +1,51 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; +import java.util.Map; + +public class HatchetArtistInfo extends Mappable { + + public String disambiguation; + + public List images; + + public Map links; + + public int listeners; + + public int loveCount; + + public List members; + + public String name; + + public List names; + + public List resources; + + public int totalPlays; + + public String url; + + public String wikiabstract; + + public boolean isFull; + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetImage.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetImage.java new file mode 100644 index 000000000..3455e371b --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetImage.java @@ -0,0 +1,34 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetImage extends Mappable { + + public int height; + + public String squareurl; + + public String type; + + public String url; + + public int width; + + public HatchetImage() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetLabel.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetLabel.java new file mode 100644 index 000000000..072ddac2f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetLabel.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; + +public class HatchetLabel extends Mappable { + + public String name; + + public List names; + + public HatchetLabel() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetMemberInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetMemberInfo.java new file mode 100644 index 000000000..10c02851f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetMemberInfo.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetMemberInfo extends Mappable { + + public String n; + + public HatchetTimeSpanInfo timespan; + + public HatchetMemberInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPersonInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPersonInfo.java new file mode 100644 index 000000000..bcd188ca1 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPersonInfo.java @@ -0,0 +1,40 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; + +public class HatchetPersonInfo extends Mappable { + + public String disambiguation; + + public List images; + + public HatchetTimeSpanInfo lifespan; + + public List memberships; + + public String name; + + public List names; + + public String url; + + public HatchetPersonInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogEntry.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogEntry.java new file mode 100644 index 000000000..0ebe6354f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogEntry.java @@ -0,0 +1,36 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.Date; + +public class HatchetPlaybackLogEntry { + + public String artistString; + + public String albumString; + + public String trackString; + + public String type; + + public Date timestamp; + + public HatchetPlaybackLogEntry() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogPostStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogPostStruct.java new file mode 100644 index 000000000..93db3a1fd --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaybackLogPostStruct.java @@ -0,0 +1,26 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetPlaybackLogPostStruct { + + public HatchetPlaybackLogEntry playbackLogEntry; + + public HatchetPlaybackLogPostStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntries.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntries.java new file mode 100644 index 000000000..4055a1eb6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntries.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; + +public class HatchetPlaylistEntries { + + public List playlistEntries; + + public HatchetPlaylistInfo playlist; + + public List albums; + + public List artists; + + public List playlists; + + public List tracks; + + public HatchetPlaylistEntries() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesPostStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesPostStruct.java new file mode 100644 index 000000000..350efc9f5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesPostStruct.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; + +public class HatchetPlaylistEntriesPostStruct { + + public int playlist; + + public List playlistEntries; + + public HatchetPlaylistEntriesPostStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesRequest.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesRequest.java new file mode 100644 index 000000000..40a6f3a2c --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntriesRequest.java @@ -0,0 +1,34 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.Date; + +public class HatchetPlaylistEntriesRequest { + + public String albumString; + + public String artistString; + + public String trackString; + + public Date timestamp; + + public HatchetPlaylistEntriesRequest() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryInfo.java new file mode 100644 index 000000000..9725ab0bb --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryInfo.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetPlaylistEntryInfo extends Mappable { + + public String album; + + public String track; + + public HatchetPlaylistEntryInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryPostStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryPostStruct.java new file mode 100644 index 000000000..ede636c50 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryPostStruct.java @@ -0,0 +1,26 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetPlaylistEntryPostStruct { + + public HatchetPlaylistEntryRequest playlistEntry; + + public HatchetPlaylistEntryPostStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryRequest.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryRequest.java new file mode 100644 index 000000000..0e76e08cf --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistEntryRequest.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.Date; + +public class HatchetPlaylistEntryRequest { + + public int playlist; + + public String albumString; + + public String artistString; + + public String trackString; + + public int position; + + public Date timestamp; + + public HatchetPlaylistEntryRequest() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistInfo.java new file mode 100644 index 000000000..31910cf5a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistInfo.java @@ -0,0 +1,43 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; +import java.util.Map; + +public class HatchetPlaylistInfo extends Mappable { + + public String created; + + public String currentrevision; + + public List playlistEntries; + + public List revisions; + + public Map links; + + public String title; + + public String user; + + public boolean isFull; + + public HatchetPlaylistInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistPostStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistPostStruct.java new file mode 100644 index 000000000..7e2f5b93c --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistPostStruct.java @@ -0,0 +1,26 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetPlaylistPostStruct { + + public HatchetPlaylistRequest playlist; + + public HatchetPlaylistPostStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistRequest.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistRequest.java new file mode 100644 index 000000000..909f4d739 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetPlaylistRequest.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; + +public class HatchetPlaylistRequest { + + public String title; + + public List playlistEntries; + + public HatchetPlaylistRequest() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipPostStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipPostStruct.java new file mode 100644 index 000000000..e7685d649 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipPostStruct.java @@ -0,0 +1,26 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetRelationshipPostStruct { + + public HatchetRelationshipStruct relationShip; + + public HatchetRelationshipPostStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipStruct.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipStruct.java new file mode 100644 index 000000000..8a336d070 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetRelationshipStruct.java @@ -0,0 +1,36 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetRelationshipStruct extends Mappable { + + public String targetAlbumString; + + public String targetArtistString; + + public String targetTrackString; + + public String targetUser; + + public String type; + + public String user; + + public HatchetRelationshipStruct() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetResourceInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetResourceInfo.java new file mode 100644 index 000000000..63197cdee --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetResourceInfo.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetResourceInfo { + + public String Type; + + public String Url; + + public HatchetResourceInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTimeSpanInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTimeSpanInfo.java new file mode 100644 index 000000000..55ea9db73 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTimeSpanInfo.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public class HatchetTimeSpanInfo { + + public String StartsAt; + + public String EndsAt; + + public HatchetTimeSpanInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTrackInfo.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTrackInfo.java new file mode 100644 index 000000000..12dff0b48 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/HatchetTrackInfo.java @@ -0,0 +1,47 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +import java.util.List; +import java.util.Map; + +public class HatchetTrackInfo extends Mappable { + + public String artist; + + public int duration; + + public Map links; + + public int listeners; + + public int loveCount; + + public String name; + + public List names; + + public int totalPlays; + + public String url; + + public boolean isFull; + + public HatchetTrackInfo() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/Mappable.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/Mappable.java new file mode 100644 index 000000000..a2b2f8dde --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/hatchet/models/Mappable.java @@ -0,0 +1,7 @@ +package org.tomahawk.libtomahawk.infosystem.hatchet.models; + +public abstract class Mappable { + + public String id; + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGenerator.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGenerator.java new file mode 100644 index 000000000..696eb73b8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGenerator.java @@ -0,0 +1,217 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.stations; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.jdeferred.Deferred; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptJob; +import org.tomahawk.libtomahawk.resolver.ScriptObject; +import org.tomahawk.libtomahawk.resolver.ScriptPlugin; +import org.tomahawk.libtomahawk.utils.ADeferredObject; + +import android.support.v4.util.Pair; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ScriptPlaylistGenerator implements ScriptPlugin { + + public static final String TAG = ScriptPlaylistGenerator.class.getSimpleName(); + + private ScriptAccount mScriptAccount; + + private ScriptObject mScriptObject; + + public ScriptPlaylistGenerator(ScriptObject scriptObject, ScriptAccount account) { + mScriptObject = scriptObject; + mScriptAccount = account; + } + + public Promise search(String query) { + Map args = new HashMap<>(); + args.put("query", query); + final Deferred deferred = + new ADeferredObject<>(); + ScriptJob.start(mScriptObject, "search", args, new ScriptJob.ResultsObjectCallback() { + @Override + public void onReportResults(JsonObject results) { + ScriptPlaylistGeneratorSearchResult result = + new ScriptPlaylistGeneratorSearchResult(); + JsonArray artists = results.getAsJsonArray("artists"); + if (artists != null) { + for (JsonElement element : artists) { + if (element instanceof JsonObject) { + JsonElement artistName = ((JsonObject) element).get("artist"); + JsonElement id = ((JsonObject) element).get("id"); + if (artistName != null) { + Artist artist = Artist.get(artistName.getAsString()); + result.mArtists.add(new Pair<>(artist, id.getAsString())); + } + } + } + } + JsonArray albums = results.getAsJsonArray("albums"); + if (albums != null) { + for (JsonElement element : albums) { + if (element instanceof JsonObject) { + JsonElement artistName = ((JsonObject) element).get("artist"); + JsonElement albumName = ((JsonObject) element).get("album"); + if (artistName != null && albumName != null) { + Artist artist = Artist.get(artistName.getAsString()); + result.mAlbums.add(Album.get(albumName.getAsString(), artist)); + } + } + } + } + JsonArray tracks = results.getAsJsonArray("tracks"); + if (tracks != null) { + for (JsonElement element : tracks) { + if (element instanceof JsonObject) { + JsonElement artistName = ((JsonObject) element).get("artist"); + JsonElement trackName = ((JsonObject) element).get("track"); + JsonElement albumName = ((JsonObject) element).get("album"); + JsonElement id = ((JsonObject) element).get("id"); + if (artistName != null && trackName != null && albumName != null) { + Artist artist = Artist.get(artistName.getAsString()); + Album album = Album.get(albumName.getAsString(), artist); + Track track = Track.get(trackName.getAsString(), album, artist); + result.mTracks.add(new Pair<>(track, id.getAsString())); + } + } + } + } + JsonArray genres = results.getAsJsonArray("genres"); + if (genres != null) { + for (JsonElement element : genres) { + if (element instanceof JsonObject) { + JsonElement genreName = ((JsonObject) element).get("name"); + if (genreName != null) { + result.mGenres.add(genreName.getAsString()); + } + } + } + } + JsonArray moods = results.getAsJsonArray("moods"); + if (moods != null) { + for (JsonElement element : moods) { + if (element instanceof JsonObject) { + JsonElement moodName = ((JsonObject) element).get("name"); + if (moodName != null) { + result.mMoods.add(moodName.getAsString()); + } + } + } + } + deferred.resolve(result); + } + }); + return deferred; + } + + public Promise fillPlaylist(String sessionId, + List> artists, List> tracks, + List genres) { + Map args = new HashMap<>(); + if (sessionId != null) { + args.put("sessionId", sessionId); + } + if (artists != null) { + JsonArray jsonArtists = new JsonArray(); + for (Pair artist : artists) { + JsonObject o = new JsonObject(); + o.addProperty("artist", artist.first.getName()); + o.addProperty("id", artist.second); + jsonArtists.add(o); + } + args.put("artists", jsonArtists); + } + if (tracks != null) { + JsonArray jsonTracks = new JsonArray(); + for (Pair track : tracks) { + JsonObject o = new JsonObject(); + o.addProperty("track", track.first.getName()); + o.addProperty("artist", track.first.getArtist().getName()); + o.addProperty("album", track.first.getAlbum().getName()); + o.addProperty("id", track.second); + jsonTracks.add(o); + } + args.put("tracks", jsonTracks); + } + if (genres != null) { + JsonArray jsonGenres = new JsonArray(); + for (String genre : genres) { + JsonObject o = new JsonObject(); + o.addProperty("name", genre); + jsonGenres.add(o); + } + args.put("genres", jsonGenres); + } + final Deferred deferred = + new ADeferredObject<>(); + ScriptJob.start(mScriptObject, "fillPlaylist", args, new ScriptJob.ResultsObjectCallback() { + @Override + public void onReportResults(JsonObject results) { + deferred.resolve(parseResult(results)); + } + }, new ScriptJob.FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + deferred.reject(new Throwable("Error while loading station: " + errormessage)); + } + }); + return deferred; + } + + private ScriptPlaylistGeneratorResult parseResult(JsonObject rawResults) { + ScriptPlaylistGeneratorResult result = new ScriptPlaylistGeneratorResult(); + result.sessionId = rawResults.get("sessionId").getAsString(); + result.results = new ArrayList<>(); + for (JsonElement element : rawResults.getAsJsonArray("results")) { + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + String trackName = object.get("track").getAsString(); + String artistName = object.get("artist").getAsString(); + String albumName = object.get("album").getAsString(); + Query query = Query.get(trackName, albumName, artistName, false); + result.results.add(query); + } + } + return result; + } + + @Override + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + @Override + public ScriptObject getScriptObject() { + return mScriptObject; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorManager.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorManager.java new file mode 100644 index 000000000..7eaf36ecd --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorManager.java @@ -0,0 +1,77 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.stations; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import java.util.HashMap; +import java.util.Map; + +import de.greenrobot.event.EventBus; + +public class ScriptPlaylistGeneratorManager { + + public static final String TAG = ScriptPlaylistGeneratorManager.class.getSimpleName(); + + private static class Holder { + + private static final ScriptPlaylistGeneratorManager + instance = new ScriptPlaylistGeneratorManager(); + + } + + public class GeneratorAddedEvent { + + } + + private Map mPlaylistGeneratorMap = new HashMap<>(); + + private ScriptPlaylistGeneratorManager() { + + } + + public static ScriptPlaylistGeneratorManager get() { + return Holder.instance; + } + + public void addPlaylistGenerator(ScriptPlaylistGenerator generator) { + mPlaylistGeneratorMap.put(generator.getScriptAccount().getName(), generator); + EventBus.getDefault().post(new GeneratorAddedEvent()); + } + + public void removePlaylistGenerator(ScriptPlaylistGenerator generator) { + mPlaylistGeneratorMap.remove(generator.getScriptAccount().getName()); + } + + public Map getAllPlaylistGenerator() { + return mPlaylistGeneratorMap; + } + + public ScriptPlaylistGenerator getPlaylistGenerator(String id) { + return mPlaylistGeneratorMap.get(id); + } + + public ScriptPlaylistGenerator getDefaultPlaylistGenerator() { + return mPlaylistGeneratorMap.get(getDefaultPlaylistGeneratorId()); + } + + public String getDefaultPlaylistGeneratorId() { + return TomahawkApp.PLUGINNAME_SPOTIFY; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorResult.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorResult.java new file mode 100644 index 000000000..2e070b46e --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorResult.java @@ -0,0 +1,33 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.stations; + +import org.tomahawk.libtomahawk.resolver.Query; + +import java.util.List; + +public class ScriptPlaylistGeneratorResult { + + public String sessionId; + + public List results; + + public ScriptPlaylistGeneratorResult() { + + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorSearchResult.java b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorSearchResult.java new file mode 100644 index 000000000..984da46cf --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/infosystem/stations/ScriptPlaylistGeneratorSearchResult.java @@ -0,0 +1,43 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.infosystem.stations; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; + +import android.support.v4.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +public class ScriptPlaylistGeneratorSearchResult { + + public List> mArtists = new ArrayList<>(); + + public List mAlbums = new ArrayList<>(); + + public List> mTracks = new ArrayList<>(); + + public List mGenres = new ArrayList<>(); + + public List mMoods = new ArrayList<>(); + + public ScriptPlaylistGeneratorSearchResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/Bitap.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Bitap.java new file mode 100644 index 000000000..8ade9e94a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Bitap.java @@ -0,0 +1,111 @@ +package org.tomahawk.libtomahawk.resolver;/* +* Diff Match and Patch +* +* Copyright 2006 Google Inc. +* http://code.google.com/p/google-diff-match-patch/ +* +* 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. +* +* Major modifications in 2015 by Enno Gottschalk +*/ + +import java.util.HashMap; +import java.util.Map; + +public class Bitap { + + public static class Result { + + public int index = -1; + + public int errors = -1; + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the Bitap algorithm. Returns + * -1 if no match found. + * + * @param text The text to search. + * @param pattern The pattern to search for. + * @return Best match index or -1. + */ + public static Result indexOf(String text, String pattern, int tolerance) { + Result result = new Result(); + + // Is there an exact match? (speedup) + int exactIndex = text.indexOf(pattern); + if (exactIndex != -1) { + result.index = exactIndex; + result.errors = 0; + return result; + } + + // Initialise the alphabet. + Map alphabet = initAlphabet(pattern); + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + + int[] last_rd = new int[0]; + for (int d = 0; d <= tolerance; d++) { + + int[] rd = new int[text.length() + pattern.length() + 2]; + rd[text.length() + pattern.length() + 1] = (1 << d) - 1; + for (int j = text.length() + pattern.length(); j > 0; j--) { + int charMatch; + if (text.length() <= j - 1 || !alphabet.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = alphabet.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + result.index = j - 1; + result.errors = d; + return result; + } + } + last_rd = rd; + } + return result; + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * + * @param pattern The text to encode. + * @return Hash of character locations. + */ + public static Map initAlphabet(String pattern) { + Map s = new HashMap<>(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/FuzzyIndex.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/FuzzyIndex.java new file mode 100644 index 000000000..348fbde70 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/FuzzyIndex.java @@ -0,0 +1,245 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.IntField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.SearcherFactory; +import org.apache.lucene.search.SearcherManager; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.util.Version; +import org.tomahawk.libtomahawk.database.CollectionDb; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.database.Cursor; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FuzzyIndex { + + private final static String TAG = FuzzyIndex.class.getSimpleName(); + + private static final String LUCENE_ROOT_FOLDER = + TomahawkApp.getContext().getFilesDir().getAbsolutePath() + File.separator + "lucene" + + File.separator; + + private static final String LAST_FUZZY_INDEX_UPDATE_SUFFIX = "_last_fuzzy_index_update"; + + private final String mLastUpdateStorageKey; + + private CollectionDb mCollectionDb; + + private String mLucenePath; + + private IndexWriter mLuceneWriter; + + private SearcherManager mSearcherManager; + + public static class IndexResult { + + public int id; + + public float score; + } + + public FuzzyIndex(CollectionDb collectionDb) { + Log.d(TAG, "FuzzyIndex constructor called: " + collectionDb.getCollectionId()); + mCollectionDb = collectionDb; + mLucenePath = LUCENE_ROOT_FOLDER + collectionDb.getCollectionId(); + mLastUpdateStorageKey = collectionDb.getCollectionId() + LAST_FUZZY_INDEX_UPDATE_SUFFIX; + ensureIndex(); + } + + /** + * Make sure that the FuzzyIndex contains all tracks that are stored in the CollectionDb. + * + * @return whether or not the process has been successful + */ + public synchronized void ensureIndex() { + Log.d(TAG, "addToIndex - using CollectionDb " + mCollectionDb.hashCode() + " with id " + + mCollectionDb.getCollectionId()); + long lastDbUpdate = mCollectionDb.getLastUpdated(); + long lastIndexUpdate = PreferenceUtils.getLong(mLastUpdateStorageKey, -2); + Log.d(TAG, "addToIndex - recreate: " + (lastDbUpdate > lastIndexUpdate)); + if (lastDbUpdate > lastIndexUpdate) { + Cursor cursor = null; + try { + String[] fields = new String[]{CollectionDb.TABLE_TRACKS + "." + CollectionDb.ID, + CollectionDb.ARTISTS_ARTIST, CollectionDb.ALBUMS_ALBUM, + CollectionDb.TRACKS_TRACK}; + cursor = mCollectionDb.tracks(null, null, fields); + beginIndexing(true); + Log.d(TAG, "addToIndex - Adding tracks to index - count: " + cursor.getCount()); + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + do { + Document document = new Document(); + document.add(new IntField("id", cursor.getInt(0), + Field.Store.YES)); + document.add(new StringField("artist", cursor.getString(1), + Field.Store.YES)); + document.add(new StringField("album", cursor.getString(2), + Field.Store.YES)); + document.add(new StringField("track", cursor.getString(3), + Field.Store.YES)); + mLuceneWriter.addDocument(document); + } while (cursor.moveToNext()); + } + PreferenceUtils.edit().putLong(mLastUpdateStorageKey, System.currentTimeMillis()) + .commit(); + } catch (IOException e) { + Log.e(TAG, "addToIndex - " + e.getClass() + ": " + e.getLocalizedMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + endIndexing(); + } + } + updateSearcherManager(); + } + + private void updateSearcherManager() { + Log.d(TAG, "updateSearcherManager"); + try { + if (mSearcherManager != null) { + mSearcherManager.close(); + } + File indexDirFile = new File(mLucenePath); + Directory dir = FSDirectory.open(indexDirFile); + mSearcherManager = new SearcherManager(dir, new SearcherFactory()); + } catch (IOException e) { + Log.e(TAG, "updateSearcherManager - " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + + public void close() { + Log.d(TAG, "close"); + endIndexing(); + if (mSearcherManager != null) { + try { + mSearcherManager.close(); + } catch (IOException e) { + Log.e(TAG, "close - " + e.getClass() + ": " + e.getLocalizedMessage()); + } + mSearcherManager = null; + } + } + + public synchronized List searchIndex(Query query) { + List indexResults = new ArrayList<>(); + try { + BooleanQuery qry = new BooleanQuery(); + if (query.isFullTextQuery()) { + String escapedQuery = MultiFieldQueryParser.escape(query.getFullTextQuery()); + Term term = new Term("track", escapedQuery); + org.apache.lucene.search.Query fqry = new FuzzyQuery(term); + qry.add(fqry, BooleanClause.Occur.SHOULD); + term = new Term("artist", escapedQuery); + fqry = new FuzzyQuery(term); + qry.add(fqry, BooleanClause.Occur.SHOULD); + term = new Term("fulltext", escapedQuery); + fqry = new FuzzyQuery(term); + qry.add(fqry, BooleanClause.Occur.SHOULD); + Log.d(TAG, "searchIndex - fulltext: " + escapedQuery); + } else { + String escapedTrackName = MultiFieldQueryParser + .escape(query.getBasicTrack().getName()); + String escapedArtistName = MultiFieldQueryParser + .escape(query.getArtist().getName()); + Term term = new Term("track", escapedTrackName); + org.apache.lucene.search.Query fqry = new FuzzyQuery(term); + qry.add(fqry, BooleanClause.Occur.MUST); + term = new Term("artist", escapedArtistName); + fqry = new FuzzyQuery(term); + qry.add(fqry, BooleanClause.Occur.MUST); + Log.d(TAG, "searchIndex - non-fulltext: " + escapedArtistName + ", " + + escapedTrackName); + } + IndexSearcher searcher = mSearcherManager.acquire(); + long time = System.currentTimeMillis(); + ScoreDoc[] hits = searcher.search(qry, 50).scoreDocs; + Log.d(TAG, + "searchIndex - searching took " + (System.currentTimeMillis() - time) + "ms"); + for (ScoreDoc doc : hits) { + Document document = searcher.doc(doc.doc); + IndexResult indexResult = new IndexResult(); + indexResult.id = document.getField("id").numericValue().intValue(); + indexResult.score = doc.score; + indexResults.add(indexResult); + } + mSearcherManager.release(searcher); + } catch (IOException e) { + Log.e(TAG, "searchIndex - " + e.getClass() + ": " + e.getLocalizedMessage()); + } + return indexResults; + } + + /** + * Initializes the IndexWriter to be able to add entries to the index. + * + * @param recreate whether or not to wipe any previously existing index + */ + private void beginIndexing(boolean recreate) throws IOException { + Log.d(TAG, "beginIndexing - recreate: " + recreate); + endIndexing(); + File indexDirFile = new File(mLucenePath); + Directory dir = FSDirectory.open(indexDirFile); + Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47); + IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_47, analyzer); + if (recreate) { + PreferenceUtils.edit().putLong(mLastUpdateStorageKey, -2).commit(); + iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE); + } else { + iwc.setOpenMode(IndexWriterConfig.OpenMode.APPEND); + } + mLuceneWriter = new IndexWriter(dir, iwc); + } + + private void endIndexing() { + Log.d(TAG, "endIndexing"); + if (mLuceneWriter != null) { + try { + mLuceneWriter.commit(); + mLuceneWriter.close(true); + mLuceneWriter = null; + } catch (IOException e) { + Log.e(TAG, "endIndexing - " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/HatchetStubResolver.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/HatchetStubResolver.java new file mode 100644 index 000000000..8833e3a2a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/HatchetStubResolver.java @@ -0,0 +1,103 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ColorTintTransformation; + +import android.graphics.drawable.ColorDrawable; +import android.widget.ImageView; + +public class HatchetStubResolver implements Resolver { + + private static class Holder { + + private static final HatchetStubResolver instance = new HatchetStubResolver(); + + } + + private HatchetStubResolver() { + } + + public static HatchetStubResolver get() { + return Holder.instance; + } + + @Override + public boolean isInitialized() { + return false; + } + + @Override + public boolean isResolving() { + return false; + } + + @Override + public void loadIcon(ImageView imageView, boolean grayOut) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_hatchet, grayOut ? R.color.disabled_resolver : 0); + } + + @Override + public void loadIconWhite(ImageView imageView, int tintColorResId) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_hatchet_white, tintColorResId); + } + + @Override + public void loadIconBackground(ImageView imageView, boolean grayOut) { + imageView.setImageDrawable(new ColorDrawable( + TomahawkApp.getContext().getResources().getColor(R.color.hatchet_resolver_bg))); + if (grayOut) { + imageView.setColorFilter(ColorTintTransformation.getColorFilter( + R.color.disabled_resolver)); + } else { + imageView.clearColorFilter(); + } + } + + @Override + public String getPrettyName() { + return HatchetAuthenticatorUtils.HATCHET_PRETTY_NAME; + } + + @Override + public void resolve(final Query queryToSearchFor) { + } + + @Override + public String getId() { + return TomahawkApp.PLUGINNAME_HATCHET; + } + + @Override + public int getWeight() { + return 0; + } + + @Override + public boolean isEnabled() { + return AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET).isLoggedIn(); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/PipeLine.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/PipeLine.java new file mode 100644 index 000000000..c336514bb --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/PipeLine.java @@ -0,0 +1,366 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.DbCollection; +import org.tomahawk.libtomahawk.collection.UserCollection; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * The {@link PipeLine} is being used to provide all the resolving functionality. All {@link + * Resolver}s are stored and invoked here. Callbacks which report the found {@link Result}s are also + * included in this class. + */ +public class PipeLine { + + private final static String TAG = PipeLine.class.getSimpleName(); + + public static final int URL_TYPE_PLAYLIST = 1; + + public static final int URL_TYPE_TRACK = 2; + + public static final int URL_TYPE_ALBUM = 3; + + public static final int URL_TYPE_ARTIST = 4; + + public static final int URL_TYPE_XSPFURL = 5; + + private static final float MINSCORE = 0.5f; + + private static final float FULLTEXT_MINSCORE = 0f; + + private static class Holder { + + private static final PipeLine instance = new PipeLine(); + + } + + public static class ResultsEvent { + + public Query mQuery; + } + + public static class UrlResultsEvent { + + public Resolver mResolver; + + public ScriptResolverUrlResult mResult; + } + + public static class ResolversChangedEvent { + + public ScriptResolver mScriptResolver; + + public boolean mManuallyAdded; + + } + + private final Set mScriptAccounts = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mManualScriptAccounts = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mResolvers = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mWaitingUrlLookups = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mWaitingQueries = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mLoadingPlugins = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private PipeLine() { + try { + String[] plugins = TomahawkApp.getContext().getAssets().list("js/resolvers"); + for (String plugin : plugins) { + String path = "/js/resolvers/" + plugin; + ScriptAccount account = new ScriptAccount(path, false); + mScriptAccounts.add(account); + mLoadingPlugins.add(account); + } + String manualResolverDirPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "manualresolvers"; + File manualResolverDir = new File(manualResolverDirPath); + plugins = manualResolverDir.list(); + if (plugins != null) { + for (String plugin : plugins) { + if (!plugin.equals(".temp")) { + String pluginPath = manualResolverDirPath + File.separator + plugin; + File pluginFile = new File(pluginPath); + if (pluginFile.isDirectory()) { + ScriptAccount account = new ScriptAccount(pluginPath, true); + mScriptAccounts.add(account); + mLoadingPlugins.add(account); + } + } + } + } + } catch (IOException e) { + Log.e(TAG, "PipeLine: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + + public static PipeLine get() { + return Holder.instance; + } + + public void onPluginLoaded(ScriptAccount account) { + mLoadingPlugins.remove(account); + if (mLoadingPlugins.isEmpty()) { + Log.d(TAG, "All plugins loaded. Resolving " + + mWaitingQueries.size() + " waiting queries. Looking up " + + mWaitingUrlLookups.size() + " waiting URLs."); + for (Query query : mWaitingQueries) { + resolve(query); + } + mWaitingQueries.clear(); + for (String url : mWaitingUrlLookups) { + lookupUrl(url); + } + mWaitingUrlLookups.clear(); + } + } + + public void addScriptAccount(ScriptAccount scriptAccount) { + mManualScriptAccounts.add(scriptAccount); + mScriptAccounts.add(scriptAccount); + mLoadingPlugins.add(scriptAccount); + } + + public void addResolver(ScriptResolver resolver) { + mResolvers.add(resolver); + ResolversChangedEvent event = new ResolversChangedEvent(); + event.mScriptResolver = resolver; + event.mManuallyAdded = mManualScriptAccounts.contains(resolver.getScriptAccount()); + EventBus.getDefault().post(event); + } + + public void removeResolver(ScriptResolver resolver) { + mResolvers.remove(resolver); + EventBus.getDefault().post(new ResolversChangedEvent()); + } + + /** + * Get the {@link ScriptResolver} with the given id, null if not found + */ + public ScriptResolver getResolver(String id) { + for (ScriptResolver resolver : mResolvers) { + if (resolver.getId().equals(id)) { + return resolver; + } + } + return null; + } + + /** + * Get the ArrayList of all {@link org.tomahawk.libtomahawk.resolver.ScriptResolver}s + */ + public ArrayList getScriptResolvers() { + ArrayList scriptResolvers = new ArrayList<>(); + for (Resolver resolver : mResolvers) { + if (resolver instanceof ScriptResolver) { + scriptResolvers.add((ScriptResolver) resolver); + } + } + return scriptResolvers; + } + + /** + * This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already + * is a {@link Query} with the same fullTextQuery, the old resultList will be reported. + */ + public Query resolve(String fullTextQuery) { + return resolve(fullTextQuery, false); + } + + /** + * This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already + * is a {@link Query} with the same fullTextQuery, the old resultList will be reported. + */ + public Query resolve(String fullTextQuery, boolean forceOnlyLocal) { + if (fullTextQuery != null && !TextUtils.isEmpty(fullTextQuery)) { + Query q = Query.get(fullTextQuery, forceOnlyLocal); + resolve(q, forceOnlyLocal); + return q; + } + return null; + } + + /** + * This will invoke every {@link Resolver} to resolve the given {@link Query}. + */ + public Query resolve(Query q) { + return resolve(q, false); + } + + /** + * This will invoke every {@link Resolver} to resolve the given {@link Query}. + */ + public Query resolve(final Query q, final boolean forceOnlyLocal) { + final TomahawkRunnable r = new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_RESOLVING) { + @Override + public void run() { + if (!mLoadingPlugins.isEmpty()) { + mWaitingQueries.add(q); + } else { + for (ScriptResolver resolver : mResolvers) { + if (shouldResolve(resolver, q, forceOnlyLocal)) { + resolver.resolve(q); + } + } + for (Collection collection : CollectionManager.get().getCollections()) { + if (!(collection instanceof UserCollection) + && shouldResolve(collection, q, forceOnlyLocal)) { + ((DbCollection) collection).resolve(q); + } + } + } + if (shouldResolve(CollectionManager.get().getUserCollection(), q, forceOnlyLocal)) { + CollectionManager.get().getUserCollection().resolve(q); + } + } + }; + ThreadManager.get().execute(r, q); + return q; + } + + /** + * Method to determine if a given Resolver should resolve the query or not + */ + public boolean shouldResolve(Collection collection, Query q, boolean forceOnlyLocal) { + return !forceOnlyLocal && !q.isOnlyLocal() && collection instanceof DbCollection + || collection instanceof UserCollection; + } + + /** + * Method to determine if a given Resolver should resolve the query or not + */ + public boolean shouldResolve(Resolver resolver, Query q, boolean forceOnlyLocal) { + return !forceOnlyLocal && !q.isOnlyLocal() && resolver.isEnabled(); + } + + /** + * Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a + * HashSet containing all query ids + */ + public HashSet resolve(Set queries) { + return resolve(queries, false); + } + + /** + * Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a + * HashSet containing all query keys + */ + public HashSet resolve(Set queries, boolean forceOnlyLocal) { + HashSet queryKeys = new HashSet<>(); + if (queries != null) { + for (Query query : queries) { + queryKeys.add(resolve(query, forceOnlyLocal)); + } + } + return queryKeys; + } + + /** + * If the {@link ScriptResolver} has resolved the {@link Query}, this method will be called. + * This method will then calculate a score and assign it to every {@link Result}. If the score + * is higher than MINSCORE the {@link Result} is added to the output resultList. + * + * @param query the {@link Query} that results are being reported for + * @param results the unfiltered {@link ArrayList} of {@link Result}s + */ + public void reportResults(final Query query, final ArrayList results, + final String resolverId) { + int priority; + if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(resolverId)) { + priority = TomahawkRunnable.PRIORITY_IS_REPORTING_LOCALSOURCE; + } else if (TomahawkApp.PLUGINNAME_SPOTIFY.equals(resolverId) + || TomahawkApp.PLUGINNAME_DEEZER.equals(resolverId) + || TomahawkApp.PLUGINNAME_BEATSMUSIC.equals(resolverId)) { + priority = TomahawkRunnable.PRIORITY_IS_REPORTING_SUBSCRIPTION; + } else { + priority = TomahawkRunnable.PRIORITY_IS_REPORTING; + } + ThreadManager.get().execute( + new TomahawkRunnable(priority) { + @Override + public void run() { + if (query != null) { + boolean shouldReport = query.isFullTextQuery(); + for (Result r : results) { + if (r != null) { + float trackScore = query.howSimilar(r); + float goalScore = query.isFullTextQuery() + ? FULLTEXT_MINSCORE : MINSCORE; + if (trackScore > goalScore) { + Result before = query.getPreferredTrackResult(); + query.addTrackResult(r, trackScore); + if (before != query.getPreferredTrackResult()) { + shouldReport = true; + } + } + } + } + if (shouldReport) { + ResultsEvent event = new ResultsEvent(); + event.mQuery = query; + EventBus.getDefault().post(event); + } + } + } + } + ); + } + + public void lookupUrl(final String url) { + Log.d(TAG, "lookupUrl - looking up url: " + url); + if (!mLoadingPlugins.isEmpty()) { + mWaitingUrlLookups.add(url); + } else { + for (Resolver resolver : mResolvers) { + if (resolver instanceof ScriptResolver) { + ScriptResolver scriptResolver = (ScriptResolver) resolver; + scriptResolver.lookupUrl(url); + } + } + } + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/Query.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Query.java new file mode 100644 index 000000000..1e7096c1f --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Query.java @@ -0,0 +1,442 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.AlphaComparable; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.ArtistAlphaComparable; +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * This class represents a query which is passed to a resolver. It contains all the information + * needed to enable the Resolver to resolve the results. + */ +public class Query extends Cacheable implements AlphaComparable, ArtistAlphaComparable { + + public static final String TAG = Query.class.getSimpleName(); + + private static final HashSet sBlacklistedResults = new HashSet<>(); + + private Track mBasicTrack; + + private String mResultHint; + + private String mFullTextQuery; + + private final boolean mIsFullTextQuery; + + private final boolean mIsOnlyLocal; + + private boolean mIsFetchedViaHatchet; + + private final ConcurrentSkipListSet mTrackResults + = new ConcurrentSkipListSet<>(new ResultComparator()); + + private final ConcurrentHashMap mTrackResultScores + = new ConcurrentHashMap<>(); + + public class ResultComparator implements Comparator { + + /** + * The actual comparison method + * + * @param r1 First {@link org.tomahawk.libtomahawk.resolver.Result} object + * @param r2 Second {@link org.tomahawk.libtomahawk.resolver.Result} Object + * @return int containing comparison score + */ + public int compare(Result r1, Result r2) { + if (mResultHint != null) { + // We have a result hint. If the cacheKey matches we automatically put the matching + // Result at the top of the sorted list. + if (r1.getCacheKey().equals(mResultHint)) { + return -1; + } else if (r2.getCacheKey().equals(mResultHint)) { + return 1; + } + } + if (r1 == r2) { + return 0; + } + Float score1 = mTrackResultScores.get(r1); + Float score2 = mTrackResultScores.get(r2); + int scoreResult = score2.compareTo(score1); + if (scoreResult > 0) { + return 1; + } else if (scoreResult < 0) { + return -1; + } else { + // We have two identical trackScores. + // Now we take the Resolver's weight into account. + Integer weight1 = r1.getResolvedBy().getWeight(); + Integer weight2 = r2.getResolvedBy().getWeight(); + int weightResult = weight2.compareTo(weight1); + if (weightResult > 0) { + return 1; + } else if (weightResult < 0) { + return -1; + } else { + // We have two identical trackScores and Resolver weights. + Integer hashCode1 = r1.hashCode(); + Integer hashCode2 = r2.hashCode(); + int hashCodeResult = hashCode1.compareTo(hashCode2); + if (hashCodeResult > 0) { + return 1; + } else if (hashCodeResult < 0) { + return -1; + } else { + // should never happen + return 0; + } + } + } + } + } + + /** + * Constructs a new Query. + * + * @param fullTextQuery fulltext-query String to construct this Query with + * @param onlyLocal whether or not this query should be resolved locally + */ + private Query(String fullTextQuery, boolean onlyLocal) { + super(Query.class, getCacheKey(fullTextQuery, onlyLocal)); + + mFullTextQuery = fullTextQuery != null ? fullTextQuery : ""; + mIsFullTextQuery = true; + mIsOnlyLocal = onlyLocal; + } + + /** + * Constructs a new Query. + * + * @param trackName track's name String + * @param artistName artist's name String + * @param albumName album's name String + * @param resultHint resultHint's name String + * @param onlyLocal whether or not this query should be resolved locally + * @param isFetchedViaHatchet whether or not this query has been fetched via the Hatchet API + */ + private Query(String trackName, String albumName, String artistName, String resultHint, + boolean onlyLocal, boolean isFetchedViaHatchet) { + super(Query.class, getCacheKey(trackName, albumName, artistName, resultHint, onlyLocal)); + + Artist artist = Artist.get(artistName); + Album album = Album.get(albumName, artist); + mBasicTrack = Track.get(trackName, album, artist); + mResultHint = resultHint != null ? resultHint : ""; + mIsFullTextQuery = false; + mIsOnlyLocal = onlyLocal; + mIsFetchedViaHatchet = isFetchedViaHatchet; + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal + */ + public static Query get(String fullTextQuery, boolean onlyLocal) { + Cacheable cacheable = get(Query.class, getCacheKey(fullTextQuery, onlyLocal)); + return cacheable != null ? (Query) cacheable : new Query(fullTextQuery, onlyLocal); + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal + */ + public static Query get(String trackName, String albumName, String artistName, + String resultHint, boolean onlyLocal, boolean isFetchedViaHatchet) { + Cacheable cacheable = get(Query.class, + getCacheKey(trackName, albumName, artistName, resultHint, onlyLocal)); + return cacheable != null ? (Query) cacheable : + new Query(trackName, albumName, artistName, resultHint, onlyLocal, + isFetchedViaHatchet); + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal + */ + public static Query get(String trackName, String albumName, String artistName, + boolean onlyLocal) { + return get(trackName, albumName, artistName, null, onlyLocal, false); + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint/onlyLocal + */ + public static Query get(String trackName, String albumName, String artistName, + boolean onlyLocal, boolean isFetchedViaHatchet) { + return get(trackName, albumName, artistName, null, onlyLocal, isFetchedViaHatchet); + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint + */ + public static Query get(Track track, boolean onlyLocal) { + return get(track.getName(), track.getAlbum().getName(), track.getArtist().getName(), null, + onlyLocal, false); + } + + /** + * Static builder method which constructs a query or fetches it from the cache, resulting in + * Queries being unique by trackname/artistname/albumname/resulthint + */ + public static Query get(Result result, boolean onlyLocal) { + return get(result.getTrack().getName(), result.getTrack().getAlbum().getName(), + result.getTrack().getArtist().getName(), result.getCacheKey(), onlyLocal, false); + } + + public static Query getByKey(String cacheKey) { + return (Query) get(Query.class, cacheKey); + } + + public Class getMediaPlayerClass() { + if (getPreferredTrackResult() != null) { + return getPreferredTrackResult().getMediaPlayerClass(); + } else { + return null; + } + } + + public Track getBasicTrack() { + return mBasicTrack; + } + + public static HashSet getBlacklistedResults() { + return sBlacklistedResults; + } + + /** + * @return An ArrayList which contains all tracks in the resultList, sorted by score. + * Given as queries. + */ + public Playlist getResultPlaylist() { + ArrayList queries = new ArrayList<>(); + for (Result result : mTrackResults) { + if (!isOnlyLocal() || result.isLocal()) { + Query query = Query.get(result, isOnlyLocal()); + query.addTrackResult(result, mTrackResultScores.get(result)); + queries.add(query); + } + } + Playlist playlist = Playlist.fromQueryList(IdGenerator.getSessionUniqueStringId(), + mFullTextQuery, "", queries); + playlist.setFilled(true); + return playlist; + } + + public Result getPreferredTrackResult() { + for (Result trackResult : mTrackResults) { + if (trackResult.getResolvedBy().isEnabled()) { + return trackResult; + } + } + return null; + } + + public Track getPreferredTrack() { + Result result = getPreferredTrackResult(); + if (result != null) { + return result.getTrack(); + } + return mBasicTrack; + } + + /** + * Add a {@link Result} to this {@link Query}. + * + * @param result The {@link Result} which should be added + * @param trackScore the trackScore for the given {@link Result} + */ + public void addTrackResult(Result result, float trackScore) { + String cacheKey = result.getCacheKey(); + if (!sBlacklistedResults.contains(cacheKey)) { + mTrackResultScores.put(result, trackScore); + mTrackResults.add(result); + } + } + + public void blacklistTrackResult(Result result) { + sBlacklistedResults.add(result.getCacheKey()); + if (result.getCacheKey().equals(mResultHint)) { + mResultHint = null; + } + mTrackResults.remove(result); + mTrackResultScores.remove(result); + } + + public String getResultHint() { + return mResultHint; + } + + public String getTopTrackResultKey() { + Result result = getPreferredTrackResult(); + if (result != null) { + return getPreferredTrackResult().getCacheKey(); + } + return null; + } + + public String getFullTextQuery() { + return mFullTextQuery; + } + + public boolean isFullTextQuery() { + return mIsFullTextQuery; + } + + public boolean isOnlyLocal() { + return mIsOnlyLocal; + } + + public boolean isPlayable() { + return getPreferredTrackResult() != null; + } + + public boolean isSolved() { + Result result = getPreferredTrackResult(); + return result != null && result.getCacheKey().equals(mResultHint); + } + + public boolean isFetchedViaHatchet() { + return mIsFetchedViaHatchet; + } + + /** + * This method determines how similar the given result is to the search string. + */ + public float howSimilar(Result r) { + String resultArtistName = ResultScoring.cleanUpString(r.getArtist().getName(), false); + String resultAlbumName = ResultScoring.cleanUpString(r.getAlbum().getName(), false); + String resultTrackName = ResultScoring.cleanUpString(r.getTrack().getName(), false); + if (isFullTextQuery()) { + String fullTextQuery = ResultScoring.cleanUpString(mFullTextQuery, true); + float maxResult = 0f; + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultTrackName + " " + resultAlbumName + " " + resultArtistName, + fullTextQuery)); + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultTrackName + " " + resultArtistName + " " + resultAlbumName, + fullTextQuery)); + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultArtistName + " " + resultTrackName + " " + resultAlbumName, + fullTextQuery)); + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultArtistName + " " + resultAlbumName + " " + resultTrackName, + fullTextQuery)); + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultAlbumName + " " + resultArtistName + " " + resultTrackName, + fullTextQuery)); + maxResult = Math.max(maxResult, ResultScoring.calculateScore( + resultAlbumName + " " + resultTrackName + " " + resultArtistName, + fullTextQuery)); + return maxResult; + } else { + String queryArtistName = + ResultScoring.cleanUpString(mBasicTrack.getArtist().getName(), false); + float artistScore = ResultScoring.calculateScore(resultArtistName, queryArtistName); + String queryTrackName = + ResultScoring.cleanUpString(mBasicTrack.getName(), false); + float trackScore = ResultScoring.calculateScore(resultTrackName, queryTrackName); + String queryAlbumName = + ResultScoring.cleanUpString(mBasicTrack.getAlbum().getName(), false); + float albumScore; + if (queryAlbumName.isEmpty()) { + return (artistScore + trackScore) / 2; + } else { + albumScore = ResultScoring.calculateScore(resultAlbumName, queryAlbumName); + return (artistScore * 3 + albumScore + trackScore * 4) / 8; + } + } + } + + public String getName() { + if (isFullTextQuery()) { + return mFullTextQuery; + } + return getPreferredTrack().getName(); + } + + /** + * @return the name that should be displayed + */ + public String getPrettyName() { + return getName().isEmpty() ? + TomahawkApp.getContext().getResources().getString(R.string.unknown) + : getName(); + } + + public Artist getArtist() { + return getPreferredTrack().getArtist(); + } + + public Album getAlbum() { + if (mIsFetchedViaHatchet && !mBasicTrack.getAlbum().getName().isEmpty()) { + return mBasicTrack.getAlbum(); + } + return getPreferredTrack().getAlbum(); + } + + public Image getImage() { + if (getAlbum().getImage() != null && !TextUtils + .isEmpty(getAlbum().getImage().getImagePath())) { + return getAlbum().getImage(); + } else { + return getArtist().getImage(); + } + } + + public boolean hasArtistImage() { + return (getAlbum().getImage() == null + || TextUtils.isEmpty(getAlbum().getImage().getImagePath())) + && getArtist().getImage() != null; + } + + public String toShortString() { + String desc; + if (mIsFullTextQuery) { + desc = "fullTextQuery: '" + mFullTextQuery + "'"; + } else { + desc = "basic: " + mBasicTrack.toShortString() + + " - preferred: " + getPreferredTrack().toShortString(); + } + return desc + ", resultCount: " + mTrackResults.size(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "( " + toShortString() + " )@" + + Integer.toHexString(hashCode()); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/QueryComparator.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/QueryComparator.java new file mode 100644 index 000000000..1e5e01da3 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/QueryComparator.java @@ -0,0 +1,64 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import java.util.Comparator; + +/** + * This class is used to compare two {@link org.tomahawk.libtomahawk.resolver.Query}s. + */ +public class QueryComparator implements Comparator { + + //Modes which determine with which method are compared + public static final int COMPARE_ALBUMPOS = 0; + + public static final int COMPARE_ALPHA = 1; + + //Flag containing the current mode to be used + private static int mFlag = COMPARE_ALBUMPOS; + + /** + * Construct this {@link QueryComparator} + * + * @param flag The mode which determines with which method {@link org.tomahawk.libtomahawk.resolver.Query}s + * are compared + */ + public QueryComparator(int flag) { + super(); + mFlag = flag; + } + + /** + * The actual comparison method + * + * @param q1 First {@link org.tomahawk.libtomahawk.resolver.Query} object + * @param q2 Second {@link org.tomahawk.libtomahawk.resolver.Query} Object + * @return int containing comparison score + */ + public int compare(Query q1, Query q2) { + switch (mFlag) { + case COMPARE_ALBUMPOS: + Integer num1 = q1.getPreferredTrack().getAlbumPos(); + Integer num2 = q2.getPreferredTrack().getAlbumPos(); + return num1.compareTo(num2); + case COMPARE_ALPHA: + return q1.getName().compareTo(q2.getName()); + } + return 0; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/Resolver.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Resolver.java new file mode 100644 index 000000000..151243acb --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Resolver.java @@ -0,0 +1,76 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import android.widget.ImageView; + +/** + * The basic {@link Resolver} interface, which is implemented by every type of {@link Resolver} + */ +public interface Resolver { + + /** + * @return Whether or not this {@link Resolver} is ready + */ + boolean isInitialized(); + + /** + * @return Whether or not this {@link Resolver} is enabled + */ + boolean isEnabled(); + + /** + * @return Whether or not this {@link Resolver} is currently resolving + */ + boolean isResolving(); + + /** + * Load this resolver's icon into the given ImageView + */ + void loadIcon(ImageView imageView, boolean grayOut); + + /** + * Load this resolver's white icon into the given ImageView + */ + void loadIconWhite(ImageView imageView, int tintColorResId); + + /** + * Load this resolver's icon background into the given ImageView + */ + void loadIconBackground(ImageView imageView, boolean grayOut); + + /** + * @return the pretty name of this resolver + */ + String getPrettyName(); + + /** + * Resolve the given {@link Query} + */ + void resolve(Query query); + + /** + * @return this {@link Resolver}'s id + */ + String getId(); + + /** + * @return this {@link Resolver}'s weight + */ + int getWeight(); +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/Result.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Result.java new file mode 100644 index 000000000..1496e9d51 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/Result.java @@ -0,0 +1,258 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.mediaplayers.AndroidMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.DeezerMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.SpotifyMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.VLCMediaPlayer; + +/** + * This class represents a {@link Result}, which will be returned by a {@link Resolver}. + */ +public class Result extends Cacheable { + + private Class mMediaPlayerClass; + + private Artist mArtist; + + private Album mAlbum; + + private Track mTrack; + + /** + * Path of file or URL. + */ + private String mPath; + + private int mBitrate; + + private int mSize; + + private Resolver mResolvedBy; + + private boolean mIsLocal = false; + + private String mLinkUrl; + + private String mPurchaseUrl; + + private boolean isResolved; + + /** + * Construct a new {@link Result} with the given {@link Track} + */ + private Result(String url, Track track, Resolver resolvedBy) { + super(Result.class, getCacheKey(url, track.getName(), track.getAlbum().getName(), + track.getArtist().getName())); + + if (url == null) { + mPath = ""; + } else { + mPath = url; + isResolved = true; + } + mResolvedBy = resolvedBy; + if (TomahawkApp.PLUGINNAME_SPOTIFY.equals(mResolvedBy.getId())) { + mMediaPlayerClass = SpotifyMediaPlayer.class; + } else if (TomahawkApp.PLUGINNAME_DEEZER.equals(mResolvedBy.getId())) { + mMediaPlayerClass = DeezerMediaPlayer.class; + } else if (TomahawkApp.PLUGINNAME_AMZN.equals(mResolvedBy.getId())) { + mMediaPlayerClass = AndroidMediaPlayer.class; + } else { + mMediaPlayerClass = VLCMediaPlayer.class; + if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(mResolvedBy.getId())) { + mIsLocal = true; + } + } + mArtist = track.getArtist(); + mAlbum = track.getAlbum(); + mTrack = track; + } + + /** + * Construct a new {@link Result} with the given {@link Artist} + */ + private Result(Artist artist) { + super(Result.class, getCacheKey(artist.getName())); + + mArtist = artist; + } + + /** + * Construct a new {@link Result} with the given {@link Album} + */ + private Result(Album album) { + super(Result.class, getCacheKey(album.getName(), album.getArtist().getName())); + + mAlbum = album; + } + + public static Result get(String url, Track track, Resolver resolvedBy) { + Cacheable cacheable = get(Result.class, getCacheKey(url, track.getName(), + track.getAlbum().getName(), track.getArtist().getName())); + return cacheable != null ? (Result) cacheable : new Result(url, track, resolvedBy); + } + + public static Result get(Artist artist) { + Cacheable cacheable = get(Result.class, getCacheKey(artist.getName())); + return cacheable != null ? (Result) cacheable : new Result(artist); + } + + public static Result get(Album album) { + Cacheable cacheable = get(Result.class, + getCacheKey(album.getName(), album.getArtist().getName())); + return cacheable != null ? (Result) cacheable : new Result(album); + } + + public Class getMediaPlayerClass() { + return mMediaPlayerClass; + } + + /** + * @return the {@link Track} associated with this {@link Result} + */ + public Track getTrack() { + return mTrack; + } + + /** + * Set the given {@link Track} as this {@link Result}'s {@link Track} + */ + public void setTrack(Track mTrack) { + this.mTrack = mTrack; + } + + /** + * @return the {@link Artist} associated with this {@link Result} + */ + public Artist getArtist() { + return mArtist; + } + + /** + * Set the given {@link Artist} as this {@link Result}'s {@link Artist} + */ + public void setArtist(Artist mArtist) { + this.mArtist = mArtist; + } + + /** + * @return the {@link Album} associated with this {@link Result} + */ + public Album getAlbum() { + return mAlbum; + } + + /** + * Set the given {@link Album} as this {@link Result}'s {@link Album} + */ + public void setAlbum(Album mAlbum) { + this.mAlbum = mAlbum; + } + + /** + * @return the {@link Resolver} associated with this {@link Result} + */ + public Resolver getResolvedBy() { + return mResolvedBy; + } + + /** + * @return Whether or not this Result has been resolved locally + */ + public boolean isLocal() { + return mIsLocal; + } + + /** + * @return the filePath/url to this {@link org.tomahawk.libtomahawk.resolver.Result}'s audio + * data + */ + public String getPath() { + return mPath; + } + + /** + * @return this {@link Track}'s bitrate + */ + public int getBitrate() { + return mBitrate; + } + + /** + * Set this {@link Track}'s bitrate + */ + public void setBitrate(int bitrate) { + this.mBitrate = bitrate; + } + + /** + * @return this {@link Track}'s filesize + */ + public int getSize() { + return mSize; + } + + /** + * Set this {@link Track}'s filesize + */ + public void setSize(int size) { + this.mSize = size; + } + + /** + * @return this {@link Track}'s purchase url + */ + public String getPurchaseUrl() { + return mPurchaseUrl; + } + + /** + * Set this {@link Track}'s purchase url + */ + public void setPurchaseUrl(String mPurchaseUrl) { + this.mPurchaseUrl = mPurchaseUrl; + } + + /** + * @return this {@link Track}'s link url + */ + public String getLinkUrl() { + return mLinkUrl; + } + + /** + * Set this {@link Track}'s link url + */ + public void setLinkUrl(String mLinkUrl) { + this.mLinkUrl = mLinkUrl; + } + + /** + * @return whether or not this {@link Track} has been resolved + */ + public boolean isResolved() { + return isResolved; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ResultScoring.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ResultScoring.java new file mode 100644 index 000000000..3d4cd92d9 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ResultScoring.java @@ -0,0 +1,122 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import java.util.ArrayList; +import java.util.List; + +public class ResultScoring { + + private static final int ERROR_TOLERANCE_RATIO = 5; + + private static final char[] sDelimiters = + new char[]{'(', '[', '{', ' ', '\n', '-', '/', '\\', ' ', ')', '[', '}'}; + + /** + * This method determines how similar the given result is to the search string. + */ + public static float calculateScore(String result, String query) { + float totalScore = 0f; + int lastIndex = 0; + List queryParts = splitUp(query, 32); // bitap only allows a max of 32 chars per run + for (String queryPart : queryParts) { + // how many errors do we allow + int tolerance = queryPart.length() / ERROR_TOLERANCE_RATIO; + String partialResult = result.substring(lastIndex, result.length()); + Bitap.Result r = Bitap.indexOf(partialResult, queryPart, tolerance); + if (r.index >= 0) { + float errorPenalty = 0f; + if (tolerance > 0) { + // worst case 30% score penalty + errorPenalty = (float) r.errors / tolerance * .3f; + } + float patternRatio; + float denominator = (float) Math.max(result.length(), queryPart.length()); + if (denominator > 0) { + patternRatio = + (float) Math.min(result.length(), queryPart.length()) / denominator; + } else { + // both query and result are empty Strings + patternRatio = 1f; + } + totalScore += patternRatio * (1f - errorPenalty); // apply the error penalty + lastIndex = r.index + queryPart.length(); + if (lastIndex >= result.length()) { + // nothing to search for anymore + break; + } + } + } + return totalScore; + } + + private static List splitUp(String s, int maxLength) { + List parts = new ArrayList<>(); + if (s.length() <= maxLength) { + // Nothing to do + parts.add(s); + return parts; + } + boolean foundNothing = false; + while (s.length() > maxLength && !foundNothing) { + for (int i = 0; i < sDelimiters.length; i++) { + char delimiter = sDelimiters[i]; + int index = s.indexOf(delimiter, s.length() - maxLength); + if (index != -1) { + // We found a delimiter + String lastPart = s.substring(index, s.length()); + if (lastPart.length() > maxLength) { + parts.addAll(0, splitUp(lastPart, maxLength)); + } else { + parts.add(0, lastPart); + } + s = s.substring(0, index); + break; + } else if (i == sDelimiters.length - 1) { + // we weren't able to find any more delimiters + foundNothing = true; + break; + } + } + } + while (s.length() > maxLength) { + // Still too long? Well then we have to just chop it off :/ + String lastPart = s.substring(s.length() - maxLength, s.length()); + parts.add(0, lastPart); + s = s.substring(0, s.length() - maxLength); + } + // Prepend all that's left + parts.add(0, s); + return parts; + } + + /** + * Clean up the given String. + * + * @param replaceArticle whether or not the prefix "the " should be removed + * @return the clean String + */ + public static String cleanUpString(String in, boolean replaceArticle) { + String out = in.toLowerCase().trim().replaceAll("[\\s]{2,}", " "); + if (replaceArticle && out.startsWith("the ")) { + out = out.substring(4); + } + return out; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptAccount.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptAccount.java new file mode 100644 index 000000000..d0cd1cc9d --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptAccount.java @@ -0,0 +1,570 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import com.squareup.okhttp.Response; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.tomahawk.libtomahawk.database.CollectionDb; +import org.tomahawk.libtomahawk.database.CollectionDbManager; +import org.tomahawk.libtomahawk.resolver.models.ScriptInterfaceRequestOptions; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverMetaData; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverTrack; +import org.tomahawk.libtomahawk.resolver.plugins.ScriptChartProviderPluginFactory; +import org.tomahawk.libtomahawk.resolver.plugins.ScriptCollectionPluginFactory; +import org.tomahawk.libtomahawk.resolver.plugins.ScriptInfoPluginFactory; +import org.tomahawk.libtomahawk.resolver.plugins.ScriptPlaylistGeneratorFactory; +import org.tomahawk.libtomahawk.resolver.plugins.ScriptResolverPluginFactory; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.libtomahawk.utils.NetworkUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.ImageView; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.greenrobot.event.EventBus; + +public class ScriptAccount implements ScriptWebViewClient.WebViewClientReadyListener { + + private final static String TAG = ScriptAccount.class.getSimpleName(); + + public final static String SCRIPT_INTERFACE_NAME = "Tomahawk"; + + public final static String CONFIG = "config"; + + public final static String ENABLED_KEY = "_enabled_"; + + private String mPath; + + private boolean mManuallyInstalled; + + private String mName; + + private WebView mWebView; + + private HashMap mJobs = new HashMap<>(); + + private HashMap mObjects = new HashMap<>(); + + private ScriptResolverPluginFactory mResolverPluginFactory = + new ScriptResolverPluginFactory(); + + private ScriptCollectionPluginFactory mCollectionPluginFactory = + new ScriptCollectionPluginFactory(); + + private ScriptInfoPluginFactory mInfoPluginFactory = + new ScriptInfoPluginFactory(); + + private ScriptChartProviderPluginFactory mChartsProviderPluginFactory = + new ScriptChartProviderPluginFactory(); + + private ScriptPlaylistGeneratorFactory mPlaylistGeneratorFactory = + new ScriptPlaylistGeneratorFactory(); + + private ScriptResolver mScriptResolver; + + private ScriptResolverMetaData mMetaData; + + @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"}) + public ScriptAccount(String path, boolean manuallyInstalled) { + String prefix = manuallyInstalled ? "file://" : "file:///android_asset"; + mPath = prefix + path; + mManuallyInstalled = manuallyInstalled; + String[] parts = mPath.split("/"); + mName = parts[parts.length - 1]; + InputStream inputStream = null; + try { + if (mManuallyInstalled) { + File metadataFile = new File( + path + File.separator + "content" + File.separator + "metadata.json"); + inputStream = new FileInputStream(metadataFile); + } else { + inputStream = TomahawkApp.getContext().getAssets() + .open(path.substring(1) + "/content/metadata.json"); + } + String metadataString = IOUtils.toString(inputStream, Charsets.UTF_8); + mMetaData = GsonHelper.get().fromJson(metadataString, ScriptResolverMetaData.class); + if (mMetaData == null) { + Log.e(TAG, "Couldn't read metadata.json. Cannot instantiate ScriptAccount."); + return; + } + } catch (IOException e) { + Log.e(TAG, "ScriptAccount: " + e.getClass() + ": " + e.getLocalizedMessage()); + Log.e(TAG, "Couldn't read metadata.json. Cannot instantiate ScriptAccount."); + return; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(TAG, "ScriptAccount: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + } + + CookieManager.setAcceptFileSchemeCookies(true); + + mWebView = new WebView(TomahawkApp.getContext()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true); + } + WebSettings settings = mWebView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDatabaseEnabled(true); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { + //noinspection deprecation + settings.setDatabasePath( + TomahawkApp.getContext().getDir("databases", Context.MODE_PRIVATE) + .getPath()); + } + settings.setDomStorageEnabled(true); + mWebView.setWebChromeClient(new TomahawkWebChromeClient()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mWebView.getSettings().setAllowUniversalAccessFromFileURLs(true); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true); + } + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + //initalize WebView + String data = "" + "" + + "" + mName + "" + + "" + + "" + + ""; + if (mMetaData.manifest.scripts != null) { + for (String scriptPath : mMetaData.manifest.scripts) { + data += ""; + } + } + try { + String[] cryptoJsScripts = + TomahawkApp.getContext().getAssets().list("js/cryptojs"); + for (String scriptPath : cryptoJsScripts) { + data += ""; + } + } catch (IOException e) { + Log.e(TAG, + "ScriptResolver: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + data += "" + + "" + + "" + + "" + + "" + + ""; + mWebView.setWebViewClient(new ScriptWebViewClient(ScriptAccount.this)); + mWebView.addJavascriptInterface(new ScriptInterface(ScriptAccount.this), + SCRIPT_INTERFACE_NAME); + mWebView.loadDataWithBaseURL("file:///android_asset/test.html", data, + "text/html", null, null); + } + }); + } + + /** + * This method is being called, when the {@link ScriptWebViewClient} has completely loaded the + * given .js script. + */ + @Override + public void onWebViewClientReady() { + //TODO: Remove this hack once we can get rid of Tomahawk.resolver.instance completely + evaluateJavaScript("Tomahawk.resolver.instance = Tomahawk.resolver.instance " + + "|| Tomahawk.extend(Tomahawk.Resolver, {});" + + "Tomahawk.PluginManager.registerPlugin('" + ScriptObject.TYPE_RESOLVER + + "', Tomahawk.resolver.instance);"); + } + + public ScriptResolver getScriptResolver() { + return mScriptResolver; + } + + public void setScriptResolver(ScriptResolver scriptResolver) { + mScriptResolver = scriptResolver; + } + + public ScriptResolverMetaData getMetaData() { + return mMetaData; + } + + public String getPath() { + return mPath; + } + + public String getName() { + return mName; + } + + public void setConfig(Map config) { + String rawJsonString = GsonHelper.get().toJson(config); + PreferenceUtils.edit().putString(buildPreferenceKey(), rawJsonString).commit(); + mScriptResolver.saveUserConfig(); + } + + /** + * @return the Map containing the Config information of this resolver + */ + public Map getConfig() { + String rawJsonString = PreferenceUtils.getString(buildPreferenceKey()); + Map result = null; + if (rawJsonString != null) { + result = GsonHelper.get().fromJson(rawJsonString, Map.class); + } + if (result == null) { + result = new HashMap<>(); + } + return result; + } + + public void loadIcon(ImageView imageView, boolean grayOut) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + mPath + "/content/" + mMetaData.manifest.icon, + grayOut ? R.color.disabled_resolver : 0); + } + + public void loadIconWhite(ImageView imageView, int tintColorResId) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + mPath + "/content/" + mMetaData.manifest.iconWhite, tintColorResId); + } + + public String getIconBackgroundPath() { + return mPath + "/content/" + mMetaData.manifest.iconBackground; + } + + public void loadIconBackground(ImageView imageView, boolean grayOut) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + mPath + "/content/" + mMetaData.manifest.iconBackground, + grayOut ? R.color.disabled_resolver : 0); + } + + public boolean isManuallyInstalled() { + return mManuallyInstalled; + } + + private String buildPreferenceKey() { + return mName + "_" + CONFIG; + } + + public void unregisterAllPlugins() { + //TODO: Uncomment this once we can get rid of Tomahawk.resolver.instance completely + /* + for (String objectId : mResolverPluginFactory.getScriptPlugins().keySet()) { + String json = mObjects.get(objectId).toJson(); + evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + + ScriptObject.TYPE_RESOLVER + "', " + json + ");"); + } + */ + for (String objectId : mCollectionPluginFactory.getScriptPlugins().keySet()) { + String json = mObjects.get(objectId).toJson(); + evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + + ScriptObject.TYPE_COLLECTION + "', " + json + ");"); + } + for (String objectId : mInfoPluginFactory.getScriptPlugins().keySet()) { + String json = mObjects.get(objectId).toJson(); + evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + + ScriptObject.TYPE_INFOPLUGIN + "', " + json + ");"); + } + } + + public void startJob(final ScriptJob job) { + final String requestId = IdGenerator.getSessionUniqueStringId(); + mJobs.put(requestId, job); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + evaluateJavaScript("Tomahawk.PluginManager.invoke(" + + "'" + requestId + "'," + + "'" + job.getScriptObject().getId() + "'," + + "'" + job.getMethodName() + "'," + + GsonHelper.get().toJson(job.getArguments()) + ")"); + } + }); + } + + private void evaluateJavaScript(final String code) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + mWebView.loadUrl("javascript: " + code); + } + }); + } + + public void reportScriptJobResult(JsonObject result) { + JsonElement requestIdNode = result.get("requestId"); + String requestId = null; + if (requestIdNode != null && requestIdNode.isJsonPrimitive()) { + requestId = result.get("requestId").getAsString(); + } + if (requestId != null && !requestId.isEmpty()) { + ScriptJob job = mJobs.get(requestId); + if (job != null) { + JsonElement errorNode = result.get("error"); + if (errorNode == null) { + job.reportResults(result.get("data")); + } else if (errorNode.isJsonPrimitive()) { + job.reportFailure(result.get("error").getAsString()); + } else { + job.reportFailure("no error message provided"); + } + } else { + Log.e(TAG, "reportScriptJobResult - ScriptAccount:" + mName + + ", couldn't find ScriptJob with given requestId"); + } + } else { + Log.e(TAG, "reportScriptJobResult - ScriptAccount:" + mName + + ", requestId is null or empty"); + } + } + + public void registerScriptPlugin(String type, String objectId) { + ScriptObject object = mObjects.get(objectId); + if (object == null) { + object = new ScriptObject(objectId, this); + mObjects.put(objectId, object); + } + switch (type) { + case ScriptObject.TYPE_RESOLVER: + mResolverPluginFactory.registerPlugin(object, this); + PipeLine.get().onPluginLoaded(this); + break; + case ScriptObject.TYPE_COLLECTION: + mCollectionPluginFactory.registerPlugin(object, this); + break; + case ScriptObject.TYPE_INFOPLUGIN: + mInfoPluginFactory.registerPlugin(object, this); + break; + case ScriptObject.TYPE_CHARTSPROVIDER: + mChartsProviderPluginFactory.registerPlugin(object, this); + break; + case ScriptObject.TYPE_PLAYLISTGENERATOR: + mPlaylistGeneratorFactory.registerPlugin(object, this); + break; + default: + Log.e(TAG, "registerScriptPlugin - ScriptAccount:" + mName + + ", ScriptPlugin type not supported!"); + } + } + + public void unregisterScriptPlugin(String type, String objectId) { + ScriptObject object = mObjects.get(objectId); + if (object == null) { + Log.e(TAG, "unregisterScriptPlugin - ScriptAccount:" + mName + + ", tried to unregister a plugin that was not registered!"); + } else { + switch (type) { + case ScriptObject.TYPE_RESOLVER: + mResolverPluginFactory.unregisterPlugin(object); + break; + case ScriptObject.TYPE_COLLECTION: + mCollectionPluginFactory.unregisterPlugin(object); + break; + case ScriptObject.TYPE_INFOPLUGIN: + mInfoPluginFactory.unregisterPlugin(object); + break; + case ScriptObject.TYPE_CHARTSPROVIDER: + mChartsProviderPluginFactory.unregisterPlugin(object); + break; + case ScriptObject.TYPE_PLAYLISTGENERATOR: + mPlaylistGeneratorFactory.unregisterPlugin(object); + break; + default: + Log.e(TAG, "unregisterScriptPlugin - ScriptAccount:" + mName + + ", ScriptPlugin type not supported!"); + } + } + } + + public void invokeNativeScriptJob(int requestId, String methodName, String paramsString) { + JsonObject params = GsonHelper.get().fromJson(paramsString, JsonObject.class); + if (methodName.equals("collectionAddTracks")) { + String id = params.get("id").getAsString(); + List tracks = GsonHelper.get().fromJson( + params.getAsJsonArray("tracks"), + new TypeToken>() { + }.getType()); + + CollectionDb collectionDb = CollectionDbManager.get().getCollectionDb(id); + collectionDb.addTracks(tracks); + + reportNativeScriptJobResult(requestId, "'" + collectionDb.getRevision() + "'"); + } else if (methodName.equals("collectionWipe")) { + String id = params.get("id").getAsString(); + + CollectionDbManager.get().getCollectionDb(id).wipe(); + + reportNativeScriptJobResult(requestId, null); + } else if (methodName.equals("collectionRevision")) { + String id = params.get("id").getAsString(); + + CollectionDb collectionDb = CollectionDbManager.get().getCollectionDb(id); + + reportNativeScriptJobResult(requestId, "'" + collectionDb.getRevision() + "'"); + } else if (methodName.equals("collectionInitialized")) { + String id = params.get("id").getAsString(); + + CollectionDbManager.get().getCollectionDb(id).wipe(); + + reportNativeScriptJobResult(requestId, null); + } else if (methodName.equals("httpRequest")) { + ScriptInterfaceRequestOptions options = + GsonHelper.get().fromJson(paramsString, ScriptInterfaceRequestOptions.class); + + reportNativeScriptJobResult(requestId, GsonHelper.get().toJson(jsHttpRequest(options))); + } else if (methodName.equals("showWebView")) { + String url = params.get("url").getAsString(); + + // This will open up a WebViewActivity, which will call onShowWebViewFinished when + // finished + TomahawkMainActivity.ShowWebViewEvent event + = new TomahawkMainActivity.ShowWebViewEvent(); + event.mRequestid = requestId; + event.mUrl = url; + EventBus.getDefault().post(event); + } + } + + private void reportNativeScriptJobResult(int requestId, String result) { + if (result == null) { + evaluateJavaScript("Tomahawk.NativeScriptJobManager.reportNativeScriptJobResult( " + + requestId + " );"); + } else { + evaluateJavaScript("Tomahawk.NativeScriptJobManager.reportNativeScriptJobResult( " + + requestId + ", " + result + " );"); + } + } + + public void onShowWebViewFinished(int requestId, String url) { + if (url != null) { + HashMap args = new HashMap<>(); + args.put("url", url); + reportNativeScriptJobResult(requestId, GsonHelper.get().toJson(args)); + } + } + + private JsonObject jsHttpRequest(ScriptInterfaceRequestOptions options) { + Response response = null; + try { + String url = null; + Map headers = null; + String method = null; + String username = null; + String password = null; + String data = null; + boolean isTestingConfig = false; + if (options != null) { + url = options.url; + headers = options.headers; + method = options.method; + username = options.username; + password = options.password; + data = options.data; + isTestingConfig = options.isTestingConfig; + } + java.net.CookieManager cookieManager = getCookieManager(isTestingConfig); + response = NetworkUtils.httpRequest(method, url, headers, username, password, data, + true, cookieManager); + // We have to encode the %-chars because the Android WebView automatically decodes + // percentage-escaped chars ... for whatever reason. Seems likely that this is a bug. + String responseText = response.body().string().replace("%", "%25"); + JsonObject responseHeaders = new JsonObject(); + for (String headerName : response.headers().names()) { + String concatenatedValues = ""; + for (int i = 0; i < response.headers(headerName).size(); i++) { + if (i > 0) { + concatenatedValues += "\n"; + } + concatenatedValues += response.headers(headerName).get(i); + } + String escapedKey = headerName.toLowerCase().replace("%", "%25"); + String escapedValue = concatenatedValues.replace("%", "%25"); + responseHeaders.addProperty(escapedKey, escapedValue); + } + int status = response.code(); + String statusText = response.message().replace("%", "%25"); + + JsonObject result = new JsonObject(); + result.addProperty("responseText", responseText); + result.add("responseHeaders", responseHeaders); + result.addProperty("status", status); + result.addProperty("statusText", statusText); + return result; + } catch (IOException e) { + Log.e(TAG, "jsHttpRequest: " + e.getClass() + ": " + e.getLocalizedMessage()); + return null; + } finally { + if (response != null) { + try { + response.body().close(); + } catch (IOException e) { + Log.e(TAG, "jsHttpRequest: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + } + } + + public java.net.CookieManager getCookieManager(boolean isTestingConfig) { + String cookieContextId; + if (isTestingConfig) { + cookieContextId = mName + "_testConfig"; + } else { + cookieContextId = mName; + } + return NetworkUtils.getCookieManager(cookieContextId); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptInterface.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptInterface.java new file mode 100644 index 000000000..a380a7b8c --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptInterface.java @@ -0,0 +1,166 @@ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FileUtils; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverData; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.util.Log; +import android.webkit.JavascriptInterface; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * This class contains all methods that are being exposed to the javascript script inside a {@link + * ScriptResolver} object. + */ +public class ScriptInterface { + + private final static String TAG = ScriptInterface.class.getSimpleName(); + + private final ScriptAccount mScriptAccount; + + ScriptInterface(ScriptAccount scriptAccount) { + mScriptAccount = scriptAccount; + } + + /** + * This method is needed because the javascript script is expecting an exposed method which will + * return the scriptPath and config. This method is being called in tomahawk_android_pre.js + * + * @return a serialized JSON-{@link String} containing the scriptPath and config. + */ + @JavascriptInterface + public String resolverDataString() { + Map config = mScriptAccount.getConfig(); + ScriptResolverData data = new ScriptResolverData(); + data.scriptPath = mScriptAccount.getPath() + "/content/" + mScriptAccount + .getMetaData().manifest.main; + data.config = config; + return GsonHelper.get().toJson(data); + } + + /** + * A straightforward log method to write something into the Debug log. + */ + @JavascriptInterface + public void log(String message) { + Log.d(TAG, "log: " + mScriptAccount.getName() + ": " + message); + } + + /** + * This method is needed because the javascript script is expecting an exposed method which it + * can call to report its capabilities. This method is being called in tomahawk_android_pre.js + * + * @param in the int pointing to the script's capabilities + */ + @JavascriptInterface + public void nativeReportCapabilities(int in) { + // NOOP until Tomahawk Desktop drops compat for pre 0.9 resolvers + } + + @JavascriptInterface + public void addCustomUrlHandler(String protocol, String callbackFuncName, boolean isAsync) { + // NOOP until Tomahawk Desktop recognizes Resolvers as URL-Handlers without this call + } + + @JavascriptInterface + public String readBase64(String fileName) { + // We return an empty string because we don't want the base64 string containing png image + // data or stuff from config.ui. + return ""; + } + + @JavascriptInterface + public void localStorageSetItem(String key, String value) { + String dirPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "TomahawkWebViewStorage"; + boolean success = new File(dirPath).mkdirs(); + if (!success) { + Log.e(TAG, "localStorageSetItem - Wasn't able to create directory: " + dirPath); + } + try { + FileUtils.writeStringToFile(new File(dirPath + File.separator + key), value, + Charsets.UTF_8); + } catch (IOException e) { + Log.e(TAG, "setItem: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + + @JavascriptInterface + public String localStorageGetItem(String key) { + String dirPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "TomahawkWebViewStorage"; + boolean success = new File(dirPath).mkdirs(); + if (!success) { + Log.e(TAG, "localStorageGetItem - Wasn't able to create directory: " + dirPath); + } + try { + return FileUtils + .readFileToString(new File(dirPath + File.separator + key), Charsets.UTF_8); + } catch (IOException e) { + Log.e(TAG, "getItem: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + return null; + } + + @JavascriptInterface + public void localStorageRemoveItem(String key) { + String path = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "TomahawkWebViewStorage" + File.separator + key; + boolean success = new File(path).delete(); + if (!success) { + Log.e(TAG, "localStorageRemoveItem - Wasn't able to delete file: " + path); + } + } + + @JavascriptInterface + public String[] keys() { + String path = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "TomahawkWebViewStorage"; + String[] keys = new File(path).list(); + if (keys == null) { + keys = new String[]{}; + } + return keys; + } + + @JavascriptInterface + public String[] values() { + String[] keys = keys(); + String[] values = new String[keys.length]; + for (int i = 0; i < keys.length; i++) { + values[i] = localStorageGetItem(keys[i]); + } + return values; + } + + @JavascriptInterface + public void reportScriptJobResults(String resultsString) { + JsonElement node = GsonHelper.get().fromJson(resultsString, JsonElement.class); + if (node.isJsonObject()) { + mScriptAccount.reportScriptJobResult((JsonObject) node); + } + } + + @JavascriptInterface + public void registerScriptPlugin(String type, String objectId) { + mScriptAccount.registerScriptPlugin(type, objectId); + } + + @JavascriptInterface + public void unregisterScriptPlugin(String type, String objectId) { + mScriptAccount.unregisterScriptPlugin(type, objectId); + } + + @JavascriptInterface + public void invokeNativeScriptJob(int requestId, String methodName, String paramsString) { + mScriptAccount.invokeNativeScriptJob(requestId, methodName, paramsString); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptJob.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptJob.java new file mode 100644 index 000000000..bcac4df75 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptJob.java @@ -0,0 +1,276 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import org.tomahawk.libtomahawk.utils.GsonHelper; + +import android.util.Log; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * A {@link ScriptJob} is an object that is being passed to the JavaScript side to handle a certain + * action (like a login for example). The {@link ScriptJob} provides an easy way to directly get + * callback data whenever the JS method has returned and the data has been passed to the Java side + * again. + */ +public class ScriptJob { + + public static final String TAG = ScriptJob.class.getSimpleName(); + + private ScriptObject mScriptObject; + + private String mMethodName; + + private Map mArguments; + + private SuccessCallback mSuccessCallback; + + private FailureCallback mFailureCallback; + + private interface SuccessCallback { + + } + + public interface ResultsArrayCallback extends SuccessCallback { + + void onReportResults(JsonArray results); + } + + public interface ResultsObjectCallback extends SuccessCallback { + + void onReportResults(JsonObject results); + } + + public interface ResultsPrimitiveCallback extends SuccessCallback { + + void onReportResults(JsonPrimitive results); + } + + public interface ResultsEmptyCallback extends SuccessCallback { + + void onReportResults(); + } + + public static abstract class ResultsCallback implements SuccessCallback { + + private Class type; + + public ResultsCallback(Class type) { + this.type = type; + } + + public abstract void onReportResults(T results); + + public Class getType() { + return type; + } + } + + public static abstract class ResultsCollectionCallback implements SuccessCallback { + + private Type type; + + public ResultsCollectionCallback(Type type) { + this.type = type; + } + + public abstract void onReportResults(Object results); + + public Type getType() { + return type; + } + } + + public interface FailureCallback { + + void onReportFailure(String errormessage); + } + + /** + * Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link + * ScriptJob}. The {@link ScriptObject} represents the Java-{@link + * ScriptPlugin} on the JS side. + * @param methodName The name of the method that will be called on the JS side. + * @param arguments The set of arguments (parameters) that is provided to the called + * method. + * @param successCallback A callback object that will get called when the request has + * successfully returned from the JS side. + * @param failureCallback A callback object that will get called when the request has failed. + */ + public static void start(ScriptObject object, String methodName, Map arguments, + SuccessCallback successCallback, FailureCallback failureCallback) { + ScriptJob job = new ScriptJob(object, methodName, arguments, successCallback, + failureCallback); + object.getScriptAccount().startJob(job); + } + + /** + * Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link + * ScriptJob}. The {@link ScriptObject} represents the Java-{@link + * ScriptPlugin} on the JS side. + * @param methodName The name of the method that will be called on the JS side. + * @param arguments The set of arguments (parameters) that is provided to the called + * method. + * @param successCallback A callback object that will get called when the request has + * successfully returned from the JS side. + */ + public static void start(ScriptObject object, String methodName, Map arguments, + SuccessCallback successCallback) { + ScriptJob job = new ScriptJob(object, methodName, arguments, successCallback, null); + object.getScriptAccount().startJob(job); + } + + /** + * Convenience-method! Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link + * ScriptJob}. The {@link ScriptObject} represents the Java-{@link + * ScriptPlugin} on the JS side. + * @param methodName The name of the method that will be called on the JS side. + * @param successCallback A callback object that will get called when the request has + * successfully returned from the JS side. + */ + public static void start(ScriptObject object, String methodName, + SuccessCallback successCallback) { + ScriptJob job = new ScriptJob(object, methodName, null, successCallback, null); + object.getScriptAccount().startJob(job); + } + + /** + * Convenience-method! Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link + * ScriptJob}. The {@link ScriptObject} represents the Java-{@link + * ScriptPlugin} on the JS side. + * @param methodName The name of the method that will be called on the JS side. + * @param successCallback A callback object that will get called when the request has + * successfully returned from the JS side. + * @param failureCallback A callback object that will get called when the request has failed. + */ + public static void start(ScriptObject object, String methodName, + SuccessCallback successCallback, FailureCallback failureCallback) { + ScriptJob job = new ScriptJob(object, methodName, null, successCallback, failureCallback); + object.getScriptAccount().startJob(job); + } + + /** + * Convenience-method! Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link ScriptJob}. + * The {@link ScriptObject} represents the Java-{@link ScriptPlugin} on the JS + * side. + * @param methodName The name of the method that will be called on the JS side. + * @param arguments The set of arguments (parameters) that is provided to the called method. + */ + public static void start(ScriptObject object, String methodName, + Map arguments) { + ScriptJob job = new ScriptJob(object, methodName, arguments, null, null); + object.getScriptAccount().startJob(job); + } + + /** + * Convenience-method! Constructs and starts a new ScriptJob. + * + * @param object The {@link ScriptObject} that is associated with this {@link ScriptJob}. + * The {@link ScriptObject} represents the Java-{@link ScriptPlugin} on the JS + * side. + * @param methodName The name of the method that will be called on the JS side. + */ + public static void start(ScriptObject object, String methodName) { + ScriptJob job = new ScriptJob(object, methodName, null, null, null); + object.getScriptAccount().startJob(job); + } + + private ScriptJob(ScriptObject object, String methodName, Map arguments, + SuccessCallback successCallback, FailureCallback failureCallback) { + mScriptObject = object; + mMethodName = methodName; + mArguments = arguments; + mSuccessCallback = successCallback; + if (failureCallback == null) { + failureCallback = new FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + Log.e(TAG, "ScriptJob failed - ScriptAccount: " + + mScriptObject.getScriptAccount().getName() + + ", methodName: " + mMethodName + + ", arguments: " + GsonHelper.get().toJson(mArguments) + + ", errorMessage: " + errormessage); + } + }; + } + mFailureCallback = failureCallback; + } + + public ScriptObject getScriptObject() { + return mScriptObject; + } + + public String getMethodName() { + return mMethodName; + } + + public Map getArguments() { + return mArguments; + } + + /** + * This method is being called if the request was successful. + * + * @param data The returned data. + */ + public void reportResults(JsonElement data) { + if (mSuccessCallback instanceof ResultsCallback) { + ResultsCallback callback = ((ResultsCallback) mSuccessCallback); + callback.onReportResults(GsonHelper.get().fromJson(data, callback.getType())); + } else if (data instanceof JsonObject + && mSuccessCallback instanceof ResultsObjectCallback) { + ((ResultsObjectCallback) mSuccessCallback).onReportResults((JsonObject) data); + } else if (data instanceof JsonPrimitive + && mSuccessCallback instanceof ResultsPrimitiveCallback) { + ((ResultsPrimitiveCallback) mSuccessCallback).onReportResults((JsonPrimitive) data); + } else if (data instanceof JsonArray && mSuccessCallback instanceof ResultsArrayCallback) { + ((ResultsArrayCallback) mSuccessCallback).onReportResults((JsonArray) data); + } else if (mSuccessCallback instanceof ResultsEmptyCallback) { + ((ResultsEmptyCallback) mSuccessCallback).onReportResults(); + } else if (mSuccessCallback != null) { + reportFailure("Unexpected result!"); + } + } + + /** + * This method is being called if the request failed. + * + * @param errorMessage Message that describes the error that occurred. + */ + public void reportFailure(String errorMessage) { + mFailureCallback.onReportFailure(errorMessage); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptObject.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptObject.java new file mode 100644 index 000000000..620364ceb --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptObject.java @@ -0,0 +1,71 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonObject; + +import org.tomahawk.libtomahawk.utils.GsonHelper; + +import java.lang.ref.WeakReference; + +public class ScriptObject { + + public static final String TYPE_RESOLVER = "resolver"; + + public static final String TYPE_INFOPLUGIN = "infoPlugin"; + + public static final String TYPE_COLLECTION = "collection"; + + public static final String TYPE_CHARTSPROVIDER = "chartsProvider"; + + public static final String TYPE_PLAYLISTGENERATOR = "playlistGenerator"; + + private String mId; + + private ScriptAccount mScriptAccount; + + private WeakReference mScriptPlugin; + + public ScriptObject(String id, ScriptAccount account) { + mId = id; + mScriptAccount = account; + } + + public String getId() { + return mId; + } + + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + public ScriptPlugin getScriptPlugin() { + return mScriptPlugin.get(); + } + + public void setScriptPlugin(ScriptPlugin scriptPlugin) { + mScriptPlugin = new WeakReference<>(scriptPlugin); + } + + public String toJson() { + JsonObject object = new JsonObject(); + object.addProperty("id", mId); + return GsonHelper.get().toJson(object); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptPlugin.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptPlugin.java new file mode 100644 index 000000000..5d66f47cf --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptPlugin.java @@ -0,0 +1,26 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +public interface ScriptPlugin { + + ScriptObject getScriptObject(); + + ScriptAccount getScriptAccount(); + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptResolver.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptResolver.java new file mode 100644 index 000000000..491c130ba --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptResolver.java @@ -0,0 +1,489 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.reflect.TypeToken; + +import com.squareup.okhttp.Response; + +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverAccessTokenResult; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverConfigUiField; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverSettings; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverStreamUrlResult; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.libtomahawk.utils.NetworkUtils; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.WeakReferenceHandler; + +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.widget.ImageView; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * This class represents a javascript resolver. + */ +public class ScriptResolver implements Resolver, ScriptPlugin { + + private final static String TAG = ScriptResolver.class.getSimpleName(); + + public static class EnabledStateChangedEvent { + + } + + private String mId; + + private ScriptObject mScriptObject; + + private ScriptAccount mScriptAccount; + + private int mWeight; + + private int mTimeout; + + private List mConfigUi; + + private boolean mEnabled; + + private boolean mInitialized; + + private boolean mStopped; + + private final Set mWaitingUrlLookups = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mWaitingQueries = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private static final int TIMEOUT_HANDLER_MSG = 1337; + + // Handler which sets the mStopped bool to true after the timeout has occured. + // Meaning this resolver is no longer being shown as resolving. + private final TimeOutHandler mTimeOutHandler = new TimeOutHandler(this); + + private static class TimeOutHandler extends WeakReferenceHandler { + + public TimeOutHandler(ScriptResolver scriptResolver) { + super(Looper.getMainLooper(), scriptResolver); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + removeMessages(msg.what); + getReferencedObject().mStopped = true; + } + } + } + + /** + * Construct a new {@link ScriptResolver} + */ + public ScriptResolver(ScriptObject object, ScriptAccount account) { + mScriptObject = object; + mScriptAccount = account; + mScriptAccount.setScriptResolver(this); + + mInitialized = false; + mStopped = true; + mId = mScriptAccount.getName(); + if (getConfig().get(ScriptAccount.ENABLED_KEY) != null) { + mEnabled = (Boolean) getConfig().get(ScriptAccount.ENABLED_KEY); + } else { + // Enable soundcloud and jamendo by default + mEnabled = TomahawkApp.PLUGINNAME_JAMENDO.equals(mId) + || TomahawkApp.PLUGINNAME_SOUNDCLOUD.equals(mId); + } + settings(); + if (mEnabled) { + init(); + } + } + + /** + * @return whether or not this {@link Resolver} is ready + */ + @Override + public boolean isInitialized() { + return mInitialized; + } + + /** + * @return whether or not this {@link ScriptResolver} is currently resolving + */ + @Override + public boolean isResolving() { + return mInitialized && !mStopped; + } + + @Override + public void loadIcon(ImageView imageView, boolean grayOut) { + mScriptAccount.loadIcon(imageView, grayOut); + } + + @Override + public void loadIconWhite(ImageView imageView, int tintColorResId) { + mScriptAccount.loadIconWhite(imageView, tintColorResId); + } + + @Override + public void loadIconBackground(ImageView imageView, boolean grayOut) { + mScriptAccount.loadIconBackground(imageView, grayOut); + } + + @Override + public String getPrettyName() { + return mScriptAccount.getMetaData().name; + } + + @Override + public ScriptAccount getScriptAccount() { + return mScriptAccount; + } + + @Override + public ScriptObject getScriptObject() { + return mScriptObject; + } + + /** + * This method calls the js function resolver.init(). + */ + private void init() { + ScriptJob.start(mScriptObject, "init", new ScriptJob.ResultsEmptyCallback() { + @Override + public void onReportResults() { + mInitialized = true; + Log.d(TAG, "ScriptResolver " + mId + " initialized successfully."); + invokeWaitingJobs(); + } + }, new ScriptJob.FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + Log.d(TAG, "ScriptResolver " + mId + " failed to initialize."); + } + }); + } + + private synchronized void invokeWaitingJobs() { + Log.d(TAG, "Resolving " + mWaitingQueries.size() + " waiting queries. Looking up " + + mWaitingUrlLookups.size() + " waiting URLs."); + for (Query query : mWaitingQueries) { + resolve(query); + } + mWaitingQueries.clear(); + for (String url : mWaitingUrlLookups) { + lookupUrl(url); + } + mWaitingUrlLookups.clear(); + } + + /** + * This method tries to get the {@link Resolver}'s settings. + */ + private void settings() { + ScriptJob.start(mScriptObject, "settings", + new ScriptJob.ResultsCallback( + ScriptResolverSettings.class) { + @Override + public void onReportResults(ScriptResolverSettings results) { + mWeight = results.weight; + mTimeout = results.timeout * 1000; + resolverGetConfigUi(); + } + }); + } + + /** + * This method tries to save the {@link Resolver}'s UserConfig. + */ + public void saveUserConfig() { + ScriptJob.start(mScriptObject, "saveUserConfig"); + } + + /** + * This method tries to get the {@link Resolver}'s UserConfig. + */ + private void resolverGetConfigUi() { + ScriptJob.start(mScriptObject, "configUi", + new ScriptJob.ResultsArrayCallback() { + @Override + public void onReportResults(JsonArray results) { + Type type = new TypeToken>() { + }.getType(); + mConfigUi = GsonHelper.get().fromJson(results, type); + } + }); + } + + public void lookupUrl(final String url) { + if (mInitialized) { + HashMap args = new HashMap<>(); + args.put("url", url); + ScriptJob.start(mScriptObject, "lookupUrl", args, + new ScriptJob.ResultsCallback( + ScriptResolverUrlResult.class) { + @Override + public void onReportResults(ScriptResolverUrlResult results) { + Log.d(TAG, "reportUrlResult - url: " + url); + PipeLine.UrlResultsEvent event = new PipeLine.UrlResultsEvent(); + event.mResolver = ScriptResolver.this; + event.mResult = results; + EventBus.getDefault().post(event); + mStopped = true; + } + }); + } else { + mWaitingUrlLookups.add(url); + } + } + + /** + * Invoke the javascript to resolve the given {@link Query}. + * + * @param query the {@link Query} which should be resolved + */ + @Override + public void resolve(final Query query) { + if (mInitialized) { + mStopped = false; + mTimeOutHandler.removeCallbacksAndMessages(null); + mTimeOutHandler.sendEmptyMessageDelayed(TIMEOUT_HANDLER_MSG, mTimeout); + + ScriptJob.ResultsObjectCallback callback = new ScriptJob.ResultsObjectCallback() { + @Override + public void onReportResults(JsonObject results) { + JsonArray tracks = results.getAsJsonArray("tracks"); + ArrayList parsedResults = + ScriptUtils.parseResultList(ScriptResolver.this, tracks); + PipeLine.get().reportResults(query, parsedResults, mId); + mTimeOutHandler.removeCallbacksAndMessages(null); + mStopped = true; + } + }; + + if (query.isFullTextQuery()) { + HashMap args = new HashMap<>(); + args.put("query", query.getFullTextQuery()); + ScriptJob.start(mScriptObject, "_adapter_search", args, callback); + } else { + HashMap args = new HashMap<>(); + args.put("artist", query.getBasicTrack().getArtist().getName()); + args.put("album", query.getBasicTrack().getAlbum().getName()); + args.put("track", query.getBasicTrack().getName()); + ScriptJob.start(mScriptObject, "_adapter_resolve", args, callback); + } + } else { + mWaitingQueries.add(query); + } + } + + public Promise getStreamUrl(final Result result) { + final ADeferredObject deferred = new ADeferredObject<>(); + if (result != null) { + HashMap args = new HashMap<>(); + args.put("url", result.getPath()); + ScriptJob.start(mScriptObject, "getStreamUrl", args, + new ScriptJob.ResultsCallback( + ScriptResolverStreamUrlResult.class) { + @Override + public void onReportResults(ScriptResolverStreamUrlResult results) { + Response response = null; + try { + if (results.headers != null) { + // If headers are given we first have to resolve the url that + // the call is being redirected to + response = NetworkUtils.httpRequest("GET", + results.url, results.headers, null, null, null, false, + null); + deferred.resolve(response.header("Location")); + } else { + deferred.resolve(results.url); + } + } catch (IOException e) { + Log.e(TAG, "reportStreamUrl: " + e.getClass() + ": " + e + .getLocalizedMessage()); + deferred.reject(e); + } finally { + if (response != null) { + try { + response.body().close(); + } catch (IOException e) { + Log.e(TAG, "getStreamUrl: " + e.getClass() + ": " + + e.getLocalizedMessage()); + } + } + } + } + }, new ScriptJob.FailureCallback() { + @Override + public void onReportFailure(String errormessage) { + deferred.reject(new Throwable(errormessage)); + } + }); + } else { + deferred.reject(new Throwable("result is null")); + } + return deferred; + } + + public void login() { + ScriptJob.start(mScriptObject, "login", null, new ScriptJob.ResultsPrimitiveCallback() { + @Override + public void onReportResults(JsonPrimitive results) { + onTestConfigFinished(results); + } + }); + } + + public void logout() { + ScriptJob.start(mScriptObject, "logout", null, new ScriptJob.ResultsPrimitiveCallback() { + @Override + public void onReportResults(JsonPrimitive results) { + onTestConfigFinished(results); + } + }); + } + + /** + * @return this {@link ScriptResolver}'s id + */ + @Override + public String getId() { + return mId; + } + + public String getName() { + return mScriptAccount.getMetaData().name; + } + + public void setConfig(Map config) { + mScriptAccount.setConfig(config); + } + + /** + * @return the Map containing the Config information of this resolver + */ + public Map getConfig() { + return mScriptAccount.getConfig(); + } + + /** + * @return this {@link ScriptResolver}'s weight + */ + @Override + public int getWeight() { + return mWeight; + } + + public String getDescription() { + return mScriptAccount.getMetaData().description; + } + + public List getConfigUi() { + return mConfigUi; + } + + @Override + public boolean isEnabled() { + AuthenticatorUtils utils = AuthenticatorManager.get().getAuthenticatorUtils(mId); + if (utils != null) { + return utils.isLoggedIn(); + } + return mEnabled; + } + + public void setEnabled(boolean enabled) { + Log.d(TAG, this.mId + " has been " + (enabled ? "enabled" : "disabled")); + mEnabled = enabled; + Map config = getConfig(); + config.put(ScriptAccount.ENABLED_KEY, enabled); + setConfig(config); + if (mEnabled) { + // Re-init so that all plugins are being registered again + settings(); + init(); + } else { + mScriptAccount.unregisterAllPlugins(); + } + EventBus.getDefault().post(new EnabledStateChangedEvent()); + } + + public void testConfig(Map config) { + // Always wipe all cookies in the testingConfig cookie store beforehand + mScriptAccount.getCookieManager(true).getCookieStore().removeAll(); + + ScriptJob.start(mScriptObject, "_adapter_testConfig", config, + new ScriptJob.ResultsPrimitiveCallback() { + @Override + public void onReportResults(JsonPrimitive results) { + onTestConfigFinished(results); + } + }); + } + + private void onTestConfigFinished(JsonPrimitive results) { + int type = -1; + String message = null; + if (results.isString()) { + type = AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_OTHER; + message = results.getAsString(); + } else if (results.isNumber() + && results.getAsInt() > 0 && results.getAsInt() < 8) { + type = results.getAsInt(); + } + Log.d(TAG, getName() + ": Config test result received. type: " + type + + ", message:" + message); + if (type == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS) { + setEnabled(true); + } else { + setEnabled(false); + } + AuthenticatorManager.ConfigTestResultEvent event + = new AuthenticatorManager.ConfigTestResultEvent(); + event.mComponent = ScriptResolver.this; + event.mType = type; + event.mMessage = message; + EventBus.getDefault().post(event); + AuthenticatorManager.showToast(getPrettyName(), event); + } + + public void getAccessToken(ScriptJob.ResultsCallback cb) { + ScriptJob.start(mScriptObject, "getAccessToken", cb); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptUtils.java new file mode 100644 index 000000000..aed4be55d --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptUtils.java @@ -0,0 +1,99 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; + +import java.util.ArrayList; + +public class ScriptUtils { + + /** + * Parses the given {@link JsonArray) into an {@link ArrayList} of {@link Result}s. + * + * @param resolver the {@link Resolver} which will be set in the {@link Result}'s constructor + * @param rawResults {@link JsonArray) containing the raw result information + * @return a {@link ArrayList} of {@link Result}s containing the parsed data + */ + public static ArrayList parseResultList(ScriptResolver resolver, JsonArray rawResults) { + ArrayList resultList = new ArrayList<>(); + for (JsonElement rawResult : rawResults) { + String url = getNodeChildAsText(rawResult, "url"); + String track = getNodeChildAsText(rawResult, "track"); + if (url != null && track != null) { + String artist = getNodeChildAsText(rawResult, "artist"); + String album = getNodeChildAsText(rawResult, "album"); + String purchaseUrl = getNodeChildAsText(rawResult, "purchaseUrl"); + String linkUrl = getNodeChildAsText(rawResult, "linkUrl"); + int albumpos = getNodeChildAsInt(rawResult, "albumpos"); + int discnumber = getNodeChildAsInt(rawResult, "discnumber"); + int year = getNodeChildAsInt(rawResult, "year"); + int duration = getNodeChildAsInt(rawResult, "duration"); + int bitrate = getNodeChildAsInt(rawResult, "bitrate"); + int size = getNodeChildAsInt(rawResult, "size"); + + Artist artistObj = Artist.get(artist); + Album albumObj = Album.get(album, artistObj); + Track trackObj = Track.get(track, albumObj, artistObj); + trackObj.setAlbumPos(albumpos); + trackObj.setDiscNumber(discnumber); + trackObj.setYear(year); + trackObj.setDuration(duration * 1000); + + Result result = Result.get(url, trackObj, resolver); + result.setBitrate(bitrate); + result.setSize(size); + result.setPurchaseUrl(purchaseUrl); + result.setLinkUrl(linkUrl); + result.setArtist(artistObj); + result.setAlbum(albumObj); + result.setTrack(trackObj); + + resultList.add(result); + } + } + return resultList; + } + + public static String getNodeChildAsText(JsonElement node, String fieldName) { + if (node instanceof JsonObject) { + JsonElement n = ((JsonObject) node).get(fieldName); + if (n != null && n.isJsonPrimitive()) { + return n.getAsString(); + } + } + return null; + } + + public static int getNodeChildAsInt(JsonElement node, String fieldName) { + if (node instanceof JsonObject) { + JsonElement n = ((JsonObject) node).get(fieldName); + if (n != null && n.isJsonPrimitive()) { + return n.getAsInt(); + } + } + return 0; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptWebViewClient.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptWebViewClient.java new file mode 100644 index 000000000..ed2191e4b --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/ScriptWebViewClient.java @@ -0,0 +1,46 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import android.webkit.WebView; +import android.webkit.WebViewClient; + +/** + * Basic WebViewClient, which is being used to determine, when our javascript file has been loaded + */ +public class ScriptWebViewClient extends WebViewClient { + + public interface WebViewClientReadyListener { + + /** + * This method is being called, when the {@link ScriptWebViewClient} has completely loaded + * the given .js script. + */ + void onWebViewClientReady(); + } + + private final WebViewClientReadyListener mReadyListener; + + public ScriptWebViewClient(WebViewClientReadyListener readyListener) { + mReadyListener = readyListener; + } + + public void onPageFinished(WebView view, String url) { + mReadyListener.onWebViewClientReady(); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/TomahawkWebChromeClient.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/TomahawkWebChromeClient.java new file mode 100644 index 000000000..536e55ebc --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/TomahawkWebChromeClient.java @@ -0,0 +1,42 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import android.support.annotation.NonNull; +import android.util.Log; +import android.webkit.ConsoleMessage; +import android.webkit.WebChromeClient; + +/** + * Used to provide output in the debug log. Especially in the case of an error. + */ +public class TomahawkWebChromeClient extends WebChromeClient { + + private final static String TAG = TomahawkWebChromeClient.class.getSimpleName(); + + @Override + public boolean onConsoleMessage(@NonNull ConsoleMessage cm) { + String msg = cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId(); + if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { + Log.e(TAG, msg); + } else { + Log.d(TAG, msg); + } + return true; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/UserCollectionStubResolver.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/UserCollectionStubResolver.java new file mode 100644 index 000000000..f8ebb9d86 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/UserCollectionStubResolver.java @@ -0,0 +1,122 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver; + +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.ColorTintTransformation; + +import android.graphics.drawable.ColorDrawable; +import android.widget.ImageView; + +/** + * A stub {@link Resolver} that is associated with all local tracks. + */ +public class UserCollectionStubResolver implements Resolver { + + private static class Holder { + + private static final UserCollectionStubResolver instance = new UserCollectionStubResolver(); + + } + + private UserCollectionStubResolver() { + } + + public static UserCollectionStubResolver get() { + return Holder.instance; + } + + /** + * @return whether or not this {@link Resolver} is ready + */ + @Override + public boolean isInitialized() { + return false; + } + + /** + * @return whether or not this {@link Resolver} is currently resolving + */ + @Override + public boolean isResolving() { + return false; + } + + @Override + public void loadIcon(ImageView imageView, boolean grayOut) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_hardware_smartphone, + grayOut ? R.color.disabled_resolver : android.R.color.black); + } + + @Override + public void loadIconWhite(ImageView imageView, int tintColorResId) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_hardware_smartphone, tintColorResId); + } + + @Override + public void loadIconBackground(ImageView imageView, boolean grayOut) { + imageView.setImageDrawable(new ColorDrawable( + TomahawkApp.getContext().getResources() + .getColor(R.color.local_collection_resolver_bg))); + if (grayOut) { + imageView.setColorFilter(ColorTintTransformation.getColorFilter( + R.color.disabled_resolver)); + } else { + imageView.clearColorFilter(); + } + } + + @Override + public String getPrettyName() { + return TomahawkApp.getContext().getString(R.string.local_collection_pretty_name); + } + + /** + * Resolve the given {@link Query}. + * + * @param queryToSearchFor the {@link Query} which should be resolved + */ + @Override + public void resolve(final Query queryToSearchFor) { + } + + /** + * @return this {@link UserCollectionStubResolver}'s id + */ + @Override + public String getId() { + return TomahawkApp.PLUGINNAME_USERCOLLECTION; + } + + /** + * @return this {@link UserCollectionStubResolver}'s weight + */ + @Override + public int getWeight() { + return 110; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptInterfaceRequestOptions.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptInterfaceRequestOptions.java new file mode 100644 index 000000000..40a436d27 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptInterfaceRequestOptions.java @@ -0,0 +1,40 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.Map; + +public class ScriptInterfaceRequestOptions { + + public String url; + + public Map headers; + + public String method; + + public String username; + + public String password; + + public String data; + + public boolean isTestingConfig; + + public ScriptInterfaceRequestOptions() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAccessTokenResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAccessTokenResult.java new file mode 100644 index 000000000..e4b15b5f5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAccessTokenResult.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverAccessTokenResult { + + public String accessToken; + + public String accessTokenSecret; + + public long accessTokenExpires; + + public ScriptResolverAccessTokenResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAlbumResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAlbumResult.java new file mode 100644 index 000000000..2cb93bee6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAlbumResult.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverAlbumResult { + + public String qid; + + public String artist; + + public String[] albums; + + public ScriptResolverAlbumResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAppKeysResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAppKeysResult.java new file mode 100644 index 000000000..c4f4a1c72 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverAppKeysResult.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverAppKeysResult { + + public String appKey; + + public String appSecret; + + public ScriptResolverAppKeysResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverArtistResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverArtistResult.java new file mode 100644 index 000000000..008cca062 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverArtistResult.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverArtistResult { + + public String qid; + + public String[] artists; + + public ScriptResolverArtistResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverCollectionMetaData.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverCollectionMetaData.java new file mode 100644 index 000000000..24461f2c2 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverCollectionMetaData.java @@ -0,0 +1,36 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverCollectionMetaData { + + public String id; + + public String prettyname; + + public String description; + + public String iconfile; + + public int trackcount; + + public int[] capabilities; + + public ScriptResolverCollectionMetaData() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUi.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUi.java new file mode 100644 index 000000000..1177254c1 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUi.java @@ -0,0 +1,28 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.List; + +public class ScriptResolverConfigUi { + + public List fields; + + public ScriptResolverConfigUi() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUiField.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUiField.java new file mode 100644 index 000000000..67570dcdd --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverConfigUiField.java @@ -0,0 +1,48 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.List; + +public class ScriptResolverConfigUiField { + + public static final String TYPE_TEXTVIEW = "textview"; + + public static final String TYPE_TEXTFIELD = "textfield"; + + public static final String TYPE_CHECKBOX = "checkbox"; + + public static final String TYPE_DROPDOWN = "dropdown"; + + public String id; + + public String type; + + public String text; + + public String label; + + public String defaultValue; + + public boolean isPassword; + + public List items; + + public ScriptResolverConfigUiField() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverData.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverData.java new file mode 100644 index 000000000..0293ff021 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverData.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.Map; + +public class ScriptResolverData { + + public String scriptPath; + + public Map config; + + public ScriptResolverData() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverFuzzyIndex.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverFuzzyIndex.java new file mode 100644 index 000000000..057dc598d --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverFuzzyIndex.java @@ -0,0 +1,32 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverFuzzyIndex { + + public int id; + + public String artist; + + public String album; + + public String track; + + public ScriptResolverFuzzyIndex() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaData.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaData.java new file mode 100644 index 000000000..a94be9ab6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaData.java @@ -0,0 +1,44 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverMetaData { + + public String name; + + public String pluginName; + + public String author; + + public String email; + + public String version; + + public String website; + + public String description; + + public String type; + + public ScriptResolverMetaDataManifest manifest; + + public String[] staticCapabilities; + + public ScriptResolverMetaData() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaDataManifest.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaDataManifest.java new file mode 100644 index 000000000..85cfafe46 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverMetaDataManifest.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.List; + +public class ScriptResolverMetaDataManifest { + + public String main; + + public List scripts; + + public String icon; + + public String iconWhite; + + public String iconBackground; + + public List resources; + + public ScriptResolverMetaDataManifest() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverResultEntry.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverResultEntry.java new file mode 100644 index 000000000..759a10047 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverResultEntry.java @@ -0,0 +1,50 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverResultEntry { + + public String url; + + public String artist; + + public String album; + + public String track; + + public int albumpos; + + public int discnumber; + + public String year; + + public int duration; + + public int bitrate; + + public int size; + + public String purchaseUrl; + + public String linkUrl; + + public float score; + + public ScriptResolverResultEntry() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverSettings.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverSettings.java new file mode 100644 index 000000000..ca37088f3 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverSettings.java @@ -0,0 +1,32 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverSettings { + + public String name; + + public int weight; + + public int timeout; + + public String icon; + + public ScriptResolverSettings() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverStreamUrlResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverStreamUrlResult.java new file mode 100644 index 000000000..eff3510e5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverStreamUrlResult.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +import java.util.Map; + +public class ScriptResolverStreamUrlResult { + + public String url; + + public Map headers; + + public ScriptResolverStreamUrlResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverTrack.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverTrack.java new file mode 100644 index 000000000..ef90ac3ec --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverTrack.java @@ -0,0 +1,48 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverTrack { + + public String track; + + public String album; + + public String imagePath; + + public String artist; + + public String artistDisambiguation; + + public String albumArtist; + + public String albumArtistDisambiguation; + + public String url; + + public float duration; + + public String linkUrl; + + public int albumpos; + + public long lastModified; + + public ScriptResolverTrack() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverUrlResult.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverUrlResult.java new file mode 100644 index 000000000..d37a5d925 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/models/ScriptResolverUrlResult.java @@ -0,0 +1,50 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.models; + +public class ScriptResolverUrlResult { + + public int type; + + public String track; + + public String artist; + + public String album; + + public String title; + + public String guid; + + public String info; + + public String creator; + + public String linkUrl; + + public String name; + + public String url; + + public String hint; + + public ScriptResolverUrlResult[] tracks; + + public ScriptResolverUrlResult() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptChartProviderPluginFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptChartProviderPluginFactory.java new file mode 100644 index 000000000..4246dbcef --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptChartProviderPluginFactory.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsManager; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsProvider; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; + +public class ScriptChartProviderPluginFactory extends ScriptPluginFactory { + + @Override + public ScriptChartsProvider createPlugin(ScriptObject object, ScriptAccount account) { + return new ScriptChartsProvider(object, account); + } + + @Override + public void addPlugin(ScriptChartsProvider scriptPlugin) { + ScriptChartsManager.get().addScriptChartsProvider(scriptPlugin); + } + + @Override + public void removePlugin(ScriptChartsProvider scriptPlugin) { + ScriptChartsManager.get().removeScriptChartsProvider(scriptPlugin); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptCollectionPluginFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptCollectionPluginFactory.java new file mode 100644 index 000000000..63046f491 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptCollectionPluginFactory.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; + +public class ScriptCollectionPluginFactory extends ScriptPluginFactory { + + @Override + public ScriptResolverCollection createPlugin(ScriptObject object, ScriptAccount account) { + return new ScriptResolverCollection(object, account); + } + + @Override + public void addPlugin(ScriptResolverCollection scriptPlugin) { + CollectionManager.get().addCollection(scriptPlugin); + } + + @Override + public void removePlugin(ScriptResolverCollection scriptPlugin) { + CollectionManager.get().removeCollection(scriptPlugin); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptInfoPluginFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptInfoPluginFactory.java new file mode 100644 index 000000000..2027aee0d --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptInfoPluginFactory.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.ScriptInfoPlugin; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; + +public class ScriptInfoPluginFactory extends ScriptPluginFactory { + + @Override + public ScriptInfoPlugin createPlugin(ScriptObject object, ScriptAccount account) { + return new ScriptInfoPlugin(object, account); + } + + @Override + public void addPlugin(ScriptInfoPlugin scriptPlugin) { + InfoSystem.get().addInfoPlugin(scriptPlugin); + } + + @Override + public void removePlugin(ScriptInfoPlugin scriptPlugin) { + InfoSystem.get().removeInfoPlugin(scriptPlugin); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPlaylistGeneratorFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPlaylistGeneratorFactory.java new file mode 100644 index 000000000..63da97837 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPlaylistGeneratorFactory.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGenerator; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorManager; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; + +public class ScriptPlaylistGeneratorFactory extends ScriptPluginFactory { + + @Override + public ScriptPlaylistGenerator createPlugin(ScriptObject object, ScriptAccount account) { + return new ScriptPlaylistGenerator(object, account); + } + + @Override + public void addPlugin(ScriptPlaylistGenerator scriptPlugin) { + ScriptPlaylistGeneratorManager.get().addPlaylistGenerator(scriptPlugin); + } + + @Override + public void removePlugin(ScriptPlaylistGenerator scriptPlugin) { + ScriptPlaylistGeneratorManager.get().removePlaylistGenerator(scriptPlugin); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPluginFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPluginFactory.java new file mode 100644 index 000000000..78fa59827 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptPluginFactory.java @@ -0,0 +1,68 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; + +import java.util.HashMap; + +public abstract class ScriptPluginFactory { + + private HashMap mScriptPlugins = new HashMap<>(); + + public void registerPlugin(ScriptObject object, ScriptAccount account) { + if (!mScriptPlugins.containsKey(object.getId())) { + T scriptPlugin = createPlugin(object, account); + if (scriptPlugin != null) { + mScriptPlugins.put(object.getId(), scriptPlugin); + } + addPlugin(scriptPlugin); + } + } + + public void unregisterPlugin(ScriptObject object) { + T scriptPlugin = mScriptPlugins.get(object.getId()); + if (scriptPlugin != null) { + removePlugin(scriptPlugin); + mScriptPlugins.remove(object.getId()); + } + } + + public abstract T createPlugin(ScriptObject object, ScriptAccount account); + + public void addAllPlugins() { + for (T plugin : mScriptPlugins.values()) { + addPlugin(plugin); + } + } + + public abstract void addPlugin(T scriptPlugin); + + public void removeAllPlugins() { + for (T plugin : mScriptPlugins.values()) { + removePlugin(plugin); + } + } + + public abstract void removePlugin(T scriptPlugin); + + public HashMap getScriptPlugins() { + return mScriptPlugins; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptResolverPluginFactory.java b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptResolverPluginFactory.java new file mode 100644 index 000000000..7b796d634 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/resolver/plugins/ScriptResolverPluginFactory.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.resolver.plugins; + +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.ScriptObject; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; + +public class ScriptResolverPluginFactory extends ScriptPluginFactory { + + @Override + public ScriptResolver createPlugin(ScriptObject object, ScriptAccount account) { + return new ScriptResolver(object, account); + } + + @Override + public void addPlugin(ScriptResolver resolver) { + PipeLine.get().addResolver(resolver); + } + + @Override + public void removePlugin(ScriptResolver resolver) { + PipeLine.get().removeResolver(resolver); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/ADeferredObject.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/ADeferredObject.java new file mode 100644 index 000000000..cd62292c4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/ADeferredObject.java @@ -0,0 +1,29 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils; + +import org.jdeferred.android.AndroidDeferredObject; +import org.jdeferred.impl.DeferredObject; + +public class ADeferredObject extends AndroidDeferredObject { + + public ADeferredObject() { + super(new DeferredObject()); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonHelper.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonHelper.java new file mode 100644 index 000000000..5ecf69142 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonHelper.java @@ -0,0 +1,66 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Date; + +public class GsonHelper { + + private static Gson mGson; + + private static class CollectionAdapter implements JsonSerializer> { + + @Override + public JsonElement serialize(Collection src, Type typeOfSrc, + JsonSerializationContext context) { + if (src == null || src.isEmpty()) // exclusion is made here + { + return null; + } + + JsonArray array = new JsonArray(); + + for (Object child : src) { + JsonElement element = context.serialize(child); + array.add(element); + } + + return array; + } + } + + public static com.google.gson.Gson get() { + if (mGson == null) { + mGson = new GsonBuilder() + .registerTypeHierarchyAdapter(Collection.class, new CollectionAdapter()) + .registerTypeAdapter(Date.class, new ISO8601DateFormat()) + .create(); + } + return mGson; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonXmlHelper.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonXmlHelper.java new file mode 100644 index 000000000..281cdd7c8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/GsonXmlHelper.java @@ -0,0 +1,50 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils; + +import com.stanfy.gsonxml.GsonXml; +import com.stanfy.gsonxml.GsonXmlBuilder; +import com.stanfy.gsonxml.XmlParserCreator; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +public class GsonXmlHelper { + + private static GsonXml mGsonXml; + + public static GsonXml get() { + if (mGsonXml == null) { + XmlParserCreator parserCreator = new XmlParserCreator() { + @Override + public XmlPullParser createParser() { + try { + return XmlPullParserFactory.newInstance().newPullParser(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + mGsonXml = new GsonXmlBuilder() + .setXmlParserCreator(parserCreator) + .create(); + } + return mGsonXml; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601DateFormat.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601DateFormat.java new file mode 100644 index 000000000..575a4f97a --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601DateFormat.java @@ -0,0 +1,34 @@ +package org.tomahawk.libtomahawk.utils; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.util.Date; + +/** + * Provide a fast thread-safe formatter/parser DateFormat for ISO8601 dates ONLY. It was mainly done + * to be used with Jackson JSON Processor.

Watch out for clone implementation that returns + * itself.

All other methods but parse and format and clone are undefined behavior. + * + * @see ISO8601Utils + */ +public class ISO8601DateFormat implements JsonDeserializer, JsonSerializer { + + @Override + public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return ISO8601Utils.parse(json.getAsString()); + } + + @Override + public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { + String value = ISO8601Utils.format(src); + return new JsonPrimitive(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601Utils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601Utils.java new file mode 100644 index 000000000..6c7c2f9ef --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/ISO8601Utils.java @@ -0,0 +1,254 @@ +package org.tomahawk.libtomahawk.utils; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC + * friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date + * objects. + */ +public class ISO8601Utils { + + /** + * ID to represent the 'GMT' string + */ + private static final String GMT_ID = "GMT"; + + /** + * The GMT timezone + */ + private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone(GMT_ID); + + /** + * Format a date into 'yyyy-MM-ddThh:mm:ssZ' (GMT timezone, no milliseconds precision) + * + * @param date the date to format + * @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ' + */ + public static String format(Date date) { + return format(date, false, TIMEZONE_GMT); + } + + /** + * Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone) + * + * @param date the date to format + * @param millis true to include millis precision otherwise false + * @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z' + */ + public static String format(Date date, boolean millis) { + return format(date, millis, TIMEZONE_GMT); + } + + /** + * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] + * + * @param date the date to format + * @param millis true to include millis precision otherwise false + * @param tz timezone to use for the formatting (GMT will produce 'Z') + * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] + */ + public static String format(Date date, boolean millis, TimeZone tz) { + Calendar calendar = new GregorianCalendar(tz, Locale.US); + calendar.setTime(date); + + // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) + int capacity = "yyyy-MM-ddThh:mm:ss".length(); + capacity += millis ? ".sss".length() : 0; + capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); + StringBuilder formatted = new StringBuilder(capacity); + + padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); + formatted.append('-'); + padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); + formatted.append('-'); + padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); + formatted.append('T'); + padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); + formatted.append(':'); + padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); + formatted.append(':'); + padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); + if (millis) { + formatted.append('.'); + padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); + } + + int offset = tz.getOffset(calendar.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + formatted.append(offset < 0 ? '-' : '+'); + padInt(formatted, hours, "hh".length()); + formatted.append(':'); + padInt(formatted, minutes, "mm".length()); + } else { + formatted.append('Z'); + } + + return formatted.toString(); + } + + + /** + * Parse a date from ISO-8601 formatted string. It expects a format + * yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] + * + * @param date ISO string to parse in the appropriate format. + * @return the parsed date + * @throws IllegalArgumentException if the date is not in the appropriate format + */ + public static Date parse(String date) { + try { + int offset = 0; + + // extract year + int year = parseInt(date, offset, offset += 4); + checkOffset(date, offset, '-'); + + // extract month + int month = parseInt(date, offset += 1, offset += 2); + checkOffset(date, offset, '-'); + + // extract day + int day = parseInt(date, offset += 1, offset += 2); + checkOffset(date, offset, 'T'); + + // extract hours, minutes, seconds and milliseconds + int hour = parseInt(date, offset += 1, offset += 2); + checkOffset(date, offset, ':'); + + int minutes = parseInt(date, offset += 1, offset += 2); + checkOffset(date, offset, ':'); + + int seconds = parseInt(date, offset += 1, offset += 2); + // milliseconds can be optional in the format + // always use 0 otherwise returned date will include millis of current time + int milliseconds = 0; + + if (date.charAt(offset) == '.') { + checkOffset(date, offset, '.'); + int digitCount = 1; + while (offset + digitCount < date.length() && digitCount < 3 + && date.charAt(offset + 1 + digitCount) != 'Z' + && date.charAt(offset + 1 + digitCount) != '+' + && date.charAt(offset + 1 + digitCount) != '-') { + digitCount++; + } + String msString = date.substring(offset += 1, offset += digitCount); + while (msString.length() < 3) { + msString += '0'; + } + milliseconds = parseInt(msString, 0, 3); + } + + // extract timezone + String timezoneId = null; + while (offset < date.length()) { + char timezoneIndicator = date.charAt(offset); + if (timezoneIndicator == '+' || timezoneIndicator == '-') { + timezoneId = GMT_ID + date.substring(offset); + break; + } else if (timezoneIndicator == 'Z') { + timezoneId = GMT_ID; + break; + } + offset++; + } + if (timezoneId == null) { + throw new IndexOutOfBoundsException("Invalid time zone indicator "); + } + TimeZone timezone = TimeZone.getTimeZone(timezoneId); + if (!timezone.getID().equals(timezoneId)) { + throw new IndexOutOfBoundsException(); + } + + Calendar calendar = new GregorianCalendar(timezone); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minutes); + calendar.set(Calendar.SECOND, seconds); + calendar.set(Calendar.MILLISECOND, milliseconds); + + return calendar.getTime(); + } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + throw new IllegalArgumentException("Failed to parse date " + date, e); + } + } + + /** + * Check if the expected character exist at the given offset of the + * + * @param value the string to check at the specified offset + * @param offset the offset to look for the expected character + * @param expected the expected character + * @throws IndexOutOfBoundsException if the expected character is not found + */ + private static void checkOffset(String value, int offset, char expected) + throws IndexOutOfBoundsException { + char found = value.charAt(offset); + if (found != expected) { + throw new IndexOutOfBoundsException( + "Expected '" + expected + "' character but found '" + found + "'"); + } + } + + /** + * Parse an integer located between 2 given offsets in a string + * + * @param value the string to parse + * @param beginIndex the start index for the integer in the string + * @param endIndex the end index for the integer in the string + * @return the int + * @throws NumberFormatException if the value is not a number + */ + private static int parseInt(String value, int beginIndex, int endIndex) + throws NumberFormatException { + if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { + throw new NumberFormatException(value); + } + // use same logic as in Integer.parseInt() but less generic we're not supporting negative values + int i = beginIndex; + int result = 0; + int digit; + if (i < endIndex) { + digit = Character.digit(value.charAt(i++), 10); + if (digit < 0) { + throw new NumberFormatException("Invalid number: " + value); + } + result = -digit; + } + while (i < endIndex) { + digit = Character.digit(value.charAt(i++), 10); + if (digit < 0) { + throw new NumberFormatException("Invalid number: " + value); + } + result *= 10; + result -= digit; + } + return -result; + } + + /** + * Zero pad a number to a specified length + * + * @param buffer buffer to use for padding + * @param value the integer value to pad if necessary. + * @param length the length of the string we should zero pad + */ + private static void padInt(StringBuilder buffer, int value, int length) { + String strValue = String.valueOf(value); + for (int i = length - strValue.length(); i > 0; i--) { + buffer.append('0'); + } + buffer.append(strValue); + } +} + diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/ImageUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/ImageUtils.java new file mode 100644 index 000000000..78d336503 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/ImageUtils.java @@ -0,0 +1,338 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils; + +import com.squareup.picasso.Callback; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.RequestCreator; +import com.squareup.picasso.Target; + +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.BlurTransformation; +import org.tomahawk.tomahawk_android.utils.ColorTintTransformation; +import org.tomahawk.tomahawk_android.utils.CropCircleTransformation; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +public class ImageUtils { + + public static final String TAG = ImageUtils.class.getSimpleName(); + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param image the path to load the image from + * @param width the width in density independent pixels to scale the image down to + */ + public static void loadImageIntoImageView(Context context, ImageView imageView, Image image, + int width, boolean isArtistImage) { + loadImageIntoImageView(context, imageView, image, width, true, isArtistImage); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param image the path to load the image from + * @param width the width in density independent pixels to scale the image down to + */ + public static void loadBlurredImageIntoImageView(Context context, ImageView imageView, + Image image, int width, int placeHolderResId) { + loadBlurredImageIntoImageView(context, imageView, image, width, placeHolderResId, null); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param image the path to load the image from + * @param width the width in density independent pixels to scale the image down to + */ + public static void loadBlurredImageIntoImageView(Context context, ImageView imageView, + Image image, int width, int placeHolderResId, Callback callback) { + RequestCreator creator; + if (image != null && !TextUtils.isEmpty(image.getImagePath())) { + String imagePath = buildImagePath(image, width); + creator = Picasso.with(context) + .load(ImageUtils.preparePathForPicasso(imagePath)) + .resize(width, width) + .transform(new BlurTransformation(context, 16)); + } else { + creator = Picasso.with(context).load(placeHolderResId); + } + if (placeHolderResId > 0) { + creator.error(placeHolderResId); + } + if (callback != null) { + creator.noFade(); + } + creator.into(imageView, callback); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param image the path to load the image from + * @param width the width in pixels to scale the image down to + */ + public static void loadImageIntoImageView(Context context, ImageView imageView, Image image, + int width, boolean fit, boolean isArtistImage) { + int placeHolder = isArtistImage ? R.drawable.artist_placeholder + : R.drawable.album_placeholder; + if (image != null && !TextUtils.isEmpty(image.getImagePath())) { + String imagePath = buildImagePath(image, width); + RequestCreator creator = Picasso.with(context).load( + ImageUtils.preparePathForPicasso(imagePath)) + .placeholder(placeHolder) + .error(placeHolder); + if (fit) { + creator.resize(width, width); + } + creator.into(imageView); + } else { + RequestCreator creator = Picasso.with(context).load(placeHolder) + .placeholder(placeHolder) + .error(placeHolder); + if (fit) { + creator.resize(width, width); + } + creator.into(imageView); + } + } + + /** + * Load a circle-shaped {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param user the user of which to load the data into the views + * @param width the width in pixels to scale the image down to + * @param textView the textview which is being used to display the first letter of the user's + * name in the placeholder image + */ + public static void loadUserImageIntoImageView(Context context, ImageView imageView, + User user, int width, TextView textView) { + int placeHolder = R.drawable.circle_black; + if (user.getImage() != null && !TextUtils.isEmpty(user.getImage().getImagePath())) { + textView.setVisibility(View.GONE); + String imagePath = buildImagePath(user.getImage(), width); + Picasso.with(context).load(ImageUtils.preparePathForPicasso(imagePath)) + .transform(new CropCircleTransformation()) + .placeholder(placeHolder) + .error(placeHolder) + .fit() + .into(imageView); + } else { + textView.setVisibility(View.VISIBLE); + textView.setText(user.getName().substring(0, 1).toUpperCase()); + Picasso.with(context).load(placeHolder) + .placeholder(placeHolder) + .error(placeHolder) + .fit() + .into(imageView); + } + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param path the path to the image + */ + public static void loadDrawableIntoImageView(Context context, ImageView imageView, + String path) { + loadDrawableIntoImageView(context, imageView, path, 0); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link + * android.graphics.Bitmap} + * @param path the path to the image + * @param colorResId the color with which to tint the imageview drawable + */ + public static void loadDrawableIntoImageView(Context context, ImageView imageView, + String path, int colorResId) { + RequestCreator creator = Picasso.with(context).load(path); + if (colorResId > 0) { + creator.transform(new ColorTintTransformation(colorResId)); + } + creator.error(R.drawable.ic_alert_error).into(imageView); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param view the {@link View}, which will be used to show the {@link + * android.graphics.Bitmap} in its background + * @param path the path to the image + * @param colorResId the color with which to tint the imageview drawable + */ + public static void loadDrawableIntoView(final Context context, final View view, + final String path, int colorResId) { + RequestCreator creator = Picasso.with(context).load(path); + if (colorResId > 0) { + creator.transform(new ColorTintTransformation(colorResId)); + } + Target target = new Target() { + @Override + public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { + view.setBackgroundDrawable(new BitmapDrawable(context.getResources(), bitmap)); + } + + @Override + public void onBitmapFailed(Drawable errorDrawable) { + Log.d(TAG, "loadDrawableIntoView onBitmapFailed for path: " + path); + } + + @Override + public void onPrepareLoad(Drawable placeHolderDrawable) { + } + }; + view.setTag(target); + creator.error(R.drawable.ic_alert_error).into(target); + } + + /** + * Load a {@link Drawable} asynchronously (convenience method) + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link Drawable} + * @param drawableResId the resource id of the drawable to load into the imageview + */ + public static void loadDrawableIntoImageView(Context context, ImageView imageView, + int drawableResId) { + loadDrawableIntoImageView(context, imageView, drawableResId, 0); + } + + /** + * Load a {@link Drawable} asynchronously + * + * @param context the context needed for fetching resources + * @param imageView the {@link ImageView}, which will be used to show the {@link Drawable} + * @param drawableResId the resource id of the drawable to load into the imageview + * @param colorResId the color with which to tint the imageview drawable + */ + public static void loadDrawableIntoImageView(Context context, ImageView imageView, + int drawableResId, int colorResId) { + RequestCreator creator = Picasso.with(context).load(drawableResId); + if (colorResId > 0) { + creator.transform(new ColorTintTransformation(colorResId)); + } + creator.error(R.drawable.ic_alert_error).into(imageView); + } + + /** + * Load a {@link android.graphics.Bitmap} asynchronously + * + * @param context the context needed for fetching resources + * @param image the path to load the image from + * @param target the Target which the loaded image will be pushed to + * @param width the width in pixels to scale the image down to + */ + public static void loadImageIntoBitmap(Context context, Image image, Target target, int width, + boolean isArtistImage) { + int placeHolder = isArtistImage ? R.drawable.artist_placeholder + : R.drawable.album_placeholder; + if (image != null && !TextUtils.isEmpty(image.getImagePath())) { + String imagePath = buildImagePath(image, width); + Picasso.with(context).load(ImageUtils.preparePathForPicasso(imagePath)) + .resize(width, width) + .into(target); + } else { + Picasso.with(context).load(placeHolder) + .resize(width, width) + .into(target); + } + } + + public static String preparePathForPicasso(String path) { + if (TextUtils.isEmpty(path) || path.contains("https://") || path.contains("http://")) { + return path; + } + return path.startsWith("file:") ? path : "file:" + path; + } + + private static String buildImagePath(Image image, int width) { + if (image.isHatchetImage()) { + int imageSize = Math.min(image.getHeight(), image.getWidth()); + int actualWidth; + if (NetworkUtils.isWifiAvailable()) { + actualWidth = Math.min(imageSize, width); + } else { + actualWidth = Math.min(imageSize, width * 2 / 3); + } + return image.getImagePath() + "?width=" + actualWidth + "&height=" + actualWidth; + } + return image.getImagePath(); + } + + @SuppressLint("NewApi") + public static void setTint(final Drawable drawable, final int colorResId) { + int color = TomahawkApp.getContext().getResources().getColor(colorResId); + drawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_ATOP); + } + + @SuppressLint("NewApi") + public static void clearTint(final Drawable drawable) { + drawable.clearColorFilter(); + } + + public static Bitmap drawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/LevensteinDistance.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/LevensteinDistance.java new file mode 100644 index 000000000..2a1ec8544 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/LevensteinDistance.java @@ -0,0 +1,108 @@ +package org.tomahawk.libtomahawk.utils; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/** + * Levenstein edit distance class. Slightly modified by Enno Gottschalk. + */ +public final class LevensteinDistance { + + /** + * Optimized to run a bit faster than the static getDistance(). + * In one benchmark times were 5.3sec using ctr vs 8.5sec w/ static method, thus 37% faster. + */ + public LevensteinDistance () { + } + + + //***************************** + // Compute Levenshtein distance: see org.apache.commons.lang.StringUtils#getLevenshteinDistance(String, String) + //***************************** + public static int getDistance (String target, String other) { + char[] sa; + int n; + int p[]; //'previous' cost array, horizontally + int d[]; // cost array, horizontally + int _d[]; //placeholder to assist in swapping p and d + + /* + The difference between this impl. and the previous is that, rather + than creating and retaining a matrix of size s.length()+1 by t.length()+1, + we maintain two single-dimensional arrays of length s.length()+1. The first, d, + is the 'current working' distance array that maintains the newest distance cost + counts as we iterate through the characters of String s. Each time we increment + the index of String t we are comparing, d is copied to p, the second int[]. Doing so + allows us to retain the previous cost counts as required by the algorithm (taking + the minimum of the cost count to the left, up one, and diagonally up and to the left + of the current cost count being calculated). (Note that the arrays aren't really + copied anymore, just switched...this is clearly much better than cloning an array + or doing a System.arraycopy() each time through the outer loop.) + + Effectively, the difference between the two implementations is this one does not + cause an out of memory condition when calculating the LD over two very large strings. + */ + + sa = target.toCharArray(); + n = sa.length; + p = new int[n+1]; + d = new int[n+1]; + + final int m = other.length(); + if (n == 0 || m == 0) { + if (n == m) { + return 1; + } + else { + return 0; + } + } + + + // indexes into strings s and t + int i; // iterates through s + int j; // iterates through t + + char t_j; // jth character of t + + int cost; // cost + + for (i = 0; i<=n; i++) { + p[i] = i; + } + + for (j = 1; j<=m; j++) { + t_j = other.charAt(j-1); + d[0] = j; + + for (i=1; i<=n; i++) { + cost = sa[i-1]==t_j ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1), p[i-1]+cost); + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + + // our last action in the above loop was to switch d and p, so p now + // actually has the most recent cost counts + return p[n]; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/NetworkUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/NetworkUtils.java new file mode 100644 index 000000000..e196a874e --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/NetworkUtils.java @@ -0,0 +1,148 @@ +package org.tomahawk.libtomahawk.utils; + +import com.squareup.okhttp.Credentials; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.logging.HttpLoggingInterceptor; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.Proxy; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class NetworkUtils { + + public static final String TAG = NetworkUtils.class.getSimpleName(); + + private static final MediaType MEDIA_TYPE_FORM = + MediaType.parse("application/x-www-form-urlencoded"); + + private static Map sCookieManagerMap = new ConcurrentHashMap<>(); + + public static CookieManager getCookieManager(String cookieContextId) { + if (sCookieManagerMap.containsKey(cookieContextId)) { + return sCookieManagerMap.get(cookieContextId); + } else { + CookieManager cookieManager = new CookieManager( + new PersistentCookieStore(TomahawkApp.getContext(), cookieContextId), + CookiePolicy.ACCEPT_ALL); + sCookieManagerMap.put(cookieContextId, cookieManager); + return cookieManager; + } + } + + /** + * Does a HTTP or HTTPS request + * + * @param method the method that should be used ("GET" or "POST"), defaults to "GET" + * (optional) + * @param urlString the complete url string to do the request with + * @param extraHeaders extra headers that should be added to the request (optional) + * @param username the username for HTTP Basic Auth (optional) + * @param password the password for HTTP Basic Auth (optional) + * @param data the body data included in POST requests (optional) + * @param followRedirects whether or not to follow redirects (also defines what is being + * returned) + * @param cookieManager the {@link CookieManager} that should be used for this request + * @return a HttpURLConnection + */ + public static Response httpRequest(String method, String urlString, + Map extraHeaders, final String username, final String password, + String data, boolean followRedirects, CookieManager cookieManager) throws IOException { + OkHttpClient client = new OkHttpClient(); + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); + client.networkInterceptors().add(loggingInterceptor); + if (cookieManager != null) { + client.setCookieHandler(cookieManager); + } + + //Set time-outs + client.setConnectTimeout(15000, TimeUnit.MILLISECONDS); + client.setReadTimeout(15000, TimeUnit.MILLISECONDS); + + client.setFollowRedirects(followRedirects); + + // Configure HTTP Basic Auth if available + if (username != null && password != null) { + client.setAuthenticator(new com.squareup.okhttp.Authenticator() { + @Override + public Request authenticate(Proxy proxy, Response response) throws IOException { + String credential = Credentials.basic(username, password); + return response.request().newBuilder().header("Authorization", credential) + .build(); + } + + @Override + public Request authenticateProxy(Proxy proxy, Response response) + throws IOException { + return null; + } + }); + } + + // Create request for remote resource. + Request.Builder builder = new Request.Builder().url(urlString); + + // Add headers if available + if (extraHeaders != null) { + for (String key : extraHeaders.keySet()) { + builder.addHeader(key, extraHeaders.get(key)); + } + } + + // Properly set up the request method. Default to GET + if (method != null) { + method = method.toUpperCase(); + } + if (method == null || method.equals("GET")) { + builder.get(); + } else { + MediaType mediaType = MEDIA_TYPE_FORM; + if (extraHeaders != null) { + String contentType = extraHeaders.get("Content-Type"); + if (contentType != null) { + mediaType = MediaType.parse(contentType); + } + } + RequestBody requestBody = null; + if (data != null) { + requestBody = RequestBody.create(mediaType, data); + } + builder.method(method, requestBody); + } + + // Build and execute the request and retrieve the response. + Request request = builder.build(); + return client.newCall(request).execute(); + } + + public static boolean isNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) + TomahawkApp.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); + } + + public static boolean isWifiAvailable() { + ConnectivityManager cm = (ConnectivityManager) + TomahawkApp.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnectedOrConnecting() + && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/PersistentCookieStore.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/PersistentCookieStore.java new file mode 100644 index 000000000..0e69be1fb --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/PersistentCookieStore.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2015 Fran Montiel + * + * 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 org.tomahawk.libtomahawk.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class PersistentCookieStore implements CookieStore { + + private static final String TAG = PersistentCookieStore.class + .getSimpleName(); + + // Persistence + private static final String SP_COOKIE_STORE_SUFFIX = "_cookieStore"; + + private static final String SP_KEY_DELIMITER = "♠"; // Unusual char in URL + + private SharedPreferences sharedPreferences; + + // In memory + private Map> allCookies; + + public PersistentCookieStore(Context context, String cookieContextId) { + sharedPreferences = context.getSharedPreferences(cookieContextId + SP_COOKIE_STORE_SUFFIX, + Context.MODE_PRIVATE); + loadAllFromPersistence(); + } + + private void loadAllFromPersistence() { + allCookies = new HashMap<>(); + + Map allPairs = sharedPreferences.getAll(); + for (Map.Entry entry : allPairs.entrySet()) { + String[] uriAndName = entry.getKey().split(SP_KEY_DELIMITER, 2); + try { + URI uri = new URI(uriAndName[0]); + String encodedCookie = (String) entry.getValue(); + HttpCookie cookie = new SerializableHttpCookie().decode(encodedCookie); + + if (cookie != null) { + Set targetCookies = allCookies.get(uri); + if (targetCookies == null) { + targetCookies = new HashSet<>(); + allCookies.put(uri, targetCookies); + } + // Repeated cookies cannot exist in persistence + // targetCookies.remove(cookie) + targetCookies.add(cookie); + } + } catch (URISyntaxException e) { + Log.w(TAG, e); + } + } + } + + @Override + public synchronized void add(URI uri, HttpCookie cookie) { + uri = cookieUri(uri, cookie); + + Set targetCookies = allCookies.get(uri); + if (targetCookies == null) { + targetCookies = new HashSet<>(); + allCookies.put(uri, targetCookies); + } + targetCookies.remove(cookie); + targetCookies.add(cookie); + + saveToPersistence(uri, cookie); + } + + /** + * Get the real URI from the cookie "domain" and "path" attributes, if they are not set then + * uses the URI provided (coming from the response) + */ + private static URI cookieUri(URI uri, HttpCookie cookie) { + URI cookieUri = uri; + if (cookie.getDomain() != null) { + // Remove the starting dot character of the domain, if exists (e.g: .domain.com -> domain.com) + String domain = cookie.getDomain(); + if (domain.charAt(0) == '.') { + domain = domain.substring(1); + } + try { + cookieUri = new URI(uri.getScheme() == null ? "http" : uri.getScheme(), domain, + cookie.getPath() == null ? "/" : cookie.getPath(), null); + } catch (URISyntaxException e) { + Log.w(TAG, e); + } + } + return cookieUri; + } + + private void saveToPersistence(URI uri, HttpCookie cookie) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + + editor.putString(uri.toString() + SP_KEY_DELIMITER + cookie.getName(), + new SerializableHttpCookie().encode(cookie)); + + editor.apply(); + } + + @Override + public synchronized List get(URI uri) { + return getValidCookies(uri); + } + + @Override + public synchronized List getCookies() { + List allValidCookies = new ArrayList<>(); + for (URI storedUri : allCookies.keySet()) { + allValidCookies.addAll(getValidCookies(storedUri)); + } + + return allValidCookies; + } + + private List getValidCookies(URI uri) { + List targetCookies = new ArrayList<>(); + // If the stored URI does not have a path then it must match any URI in + // the same domain + for (URI storedUri : allCookies.keySet()) { + // Check ith the domains match according to RFC 6265 + if (checkDomainsMatch(storedUri.getHost(), uri.getHost())) { + // Check if the paths match according to RFC 6265 + if (checkPathsMatch(storedUri.getPath(), uri.getPath())) { + targetCookies.addAll(allCookies.get(storedUri)); + } + } + } + + // Check it there are expired cookies and remove them + if (!targetCookies.isEmpty()) { + List cookiesToRemoveFromPersistence = new ArrayList<>(); + for (Iterator it = targetCookies.iterator(); it.hasNext(); ) { + HttpCookie currentCookie = it.next(); + if (currentCookie.hasExpired()) { + cookiesToRemoveFromPersistence.add(currentCookie); + it.remove(); + } + } + + if (!cookiesToRemoveFromPersistence.isEmpty()) { + removeFromPersistence(uri, cookiesToRemoveFromPersistence); + } + } + return targetCookies; + } + + /* http://tools.ietf.org/html/rfc6265#section-5.1.3 + + A string domain-matches a given domain string if at least one of the + following conditions hold: + + o The domain string and the string are identical. (Note that both + the domain string and the string will have been canonicalized to + lower case at this point.) + + o All of the following conditions hold: + + * The domain string is a suffix of the string. + + * The last character of the string that is not included in the + domain string is a %x2E (".") character. + + * The string is a host name (i.e., not an IP address). */ + + private boolean checkDomainsMatch(String cookieHost, String requestHost) { + return requestHost.equals(cookieHost) || requestHost.endsWith("." + cookieHost); + } + + /* http://tools.ietf.org/html/rfc6265#section-5.1.4 + + A request-path path-matches a given cookie-path if at least one of + the following conditions holds: + + o The cookie-path and the request-path are identical. + + o The cookie-path is a prefix of the request-path, and the last + character of the cookie-path is %x2F ("/"). + + o The cookie-path is a prefix of the request-path, and the first + character of the request-path that is not included in the cookie- + path is a %x2F ("/") character. */ + + private boolean checkPathsMatch(String cookiePath, String requestPath) { + return requestPath.equals(cookiePath) || + (requestPath.startsWith(cookiePath) + && cookiePath.charAt(cookiePath.length() - 1) == '/') || + (requestPath.startsWith(cookiePath) + && requestPath.substring(cookiePath.length()).charAt(0) == '/'); + } + + private void removeFromPersistence(URI uri, List cookiesToRemove) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + for (HttpCookie cookieToRemove : cookiesToRemove) { + editor.remove(uri.toString() + SP_KEY_DELIMITER + cookieToRemove.getName()); + } + editor.apply(); + } + + @Override + public synchronized List getURIs() { + return new ArrayList<>(allCookies.keySet()); + } + + @Override + public synchronized boolean remove(URI uri, HttpCookie cookie) { + Set targetCookies = allCookies.get(uri); + boolean cookieRemoved = targetCookies != null && targetCookies.remove(cookie); + if (cookieRemoved) { + removeFromPersistence(uri, cookie); + } + return cookieRemoved; + } + + private void removeFromPersistence(URI uri, HttpCookie cookieToRemove) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(uri.toString() + SP_KEY_DELIMITER + cookieToRemove.getName()); + editor.apply(); + } + + @Override + public synchronized boolean removeAll() { + allCookies.clear(); + removeAllFromPersistence(); + return true; + } + + private void removeAllFromPersistence() { + sharedPreferences.edit().clear().apply(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/SerializableHttpCookie.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/SerializableHttpCookie.java new file mode 100644 index 000000000..4f9747cdf --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/SerializableHttpCookie.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2011 James Smith + * Copyright (c) 2015 Fran Montiel + * + * 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 org.tomahawk.libtomahawk.utils; + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.net.HttpCookie; + +/** + * Based on the code from this stackoverflow answer http://stackoverflow.com/a/25462286/980387 by + * janoliver Modifications in the structure of the class and addition of serialization of httpOnly + * attribute + */ + +public class SerializableHttpCookie implements Serializable { + + private static final String TAG = SerializableHttpCookie.class + .getSimpleName(); + + private static final long serialVersionUID = 6374381323722046732L; + + private transient HttpCookie cookie; + + // Workaround httpOnly: The httpOnly attribute is not accessible so when we + // serialize and deserialize the cookie it not preserve the same value. We + // need to access it using reflection + private Field fieldHttpOnly; + + public SerializableHttpCookie() { + } + + public String encode(HttpCookie cookie) { + this.cookie = cookie; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ObjectOutputStream outputStream = new ObjectOutputStream(os); + outputStream.writeObject(this); + } catch (IOException e) { + Log.d(TAG, "IOException in encodeCookie", e); + return null; + } + + return byteArrayToHexString(os.toByteArray()); + } + + public HttpCookie decode(String encodedCookie) { + byte[] bytes = hexStringToByteArray(encodedCookie); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + bytes); + HttpCookie cookie = null; + try { + ObjectInputStream objectInputStream = new ObjectInputStream( + byteArrayInputStream); + cookie = ((SerializableHttpCookie) objectInputStream.readObject()).cookie; + } catch (IOException e) { + Log.d(TAG, "IOException in decodeCookie", e); + } catch (ClassNotFoundException e) { + Log.d(TAG, "ClassNotFoundException in decodeCookie", e); + } + + return cookie; + } + + // Workaround httpOnly (getter) + private boolean getHttpOnly() { + try { + initFieldHttpOnly(); + return (boolean) fieldHttpOnly.get(cookie); + } catch (Exception e) { + // NoSuchFieldException || IllegalAccessException || + // IllegalArgumentException + Log.w(TAG, e); + } + return false; + } + + // Workaround httpOnly (setter) + private void setHttpOnly(boolean httpOnly) { + try { + initFieldHttpOnly(); + fieldHttpOnly.set(cookie, httpOnly); + } catch (Exception e) { + // NoSuchFieldException || IllegalAccessException || + // IllegalArgumentException + Log.w(TAG, e); + } + } + + private void initFieldHttpOnly() throws NoSuchFieldException { + fieldHttpOnly = cookie.getClass().getDeclaredField("httpOnly"); + fieldHttpOnly.setAccessible(true); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeObject(cookie.getName()); + out.writeObject(cookie.getValue()); + out.writeObject(cookie.getComment()); + out.writeObject(cookie.getCommentURL()); + out.writeObject(cookie.getDomain()); + out.writeLong(cookie.getMaxAge()); + out.writeObject(cookie.getPath()); + out.writeObject(cookie.getPortlist()); + out.writeInt(cookie.getVersion()); + out.writeBoolean(cookie.getSecure()); + out.writeBoolean(cookie.getDiscard()); + out.writeBoolean(getHttpOnly()); + } + + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + String name = (String) in.readObject(); + String value = (String) in.readObject(); + cookie = new HttpCookie(name, value); + cookie.setComment((String) in.readObject()); + cookie.setCommentURL((String) in.readObject()); + cookie.setDomain((String) in.readObject()); + cookie.setMaxAge(in.readLong()); + cookie.setPath((String) in.readObject()); + cookie.setPortlist((String) in.readObject()); + cookie.setVersion(in.readInt()); + cookie.setSecure(in.readBoolean()); + cookie.setDiscard(in.readBoolean()); + setHttpOnly(in.readBoolean()); + } + + /** + * Using some super basic byte array <-> hex conversions so we don't have to rely on any + * large Base64 libraries. Can be overridden if you like! + * + * @param bytes byte array to be converted + * @return string containing hex values + */ + private String byteArrayToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte element : bytes) { + int v = element & 0xff; + if (v < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(v)); + } + return sb.toString(); + } + + /** + * Converts hex values from strings to byte array + * + * @param hexString string of hex-encoded values + * @return decoded byte array + */ + private byte[] hexStringToByteArray(String hexString) { + int len = hexString.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character + .digit(hexString.charAt(i + 1), 16)); + } + return data; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/StringUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/StringUtils.java new file mode 100644 index 000000000..fa1a0338c --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/StringUtils.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.tomahawk.libtomahawk.utils; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Locale; + +public class StringUtils { + + /** + *

Escapes the characters in a String using JavaScript String rules.

+ *

Escapes any values it finds into their JavaScript String form. Deals correctly with quotes + * and control-chars (tab, backslash, cr, ff, etc.)

+ * + *

So a tab becomes the characters '\\' and 't'.

+ * + *

The only difference between Java strings and JavaScript strings is that in JavaScript, a + * single quote must be escaped.

+ * + *

Example: + *

+     * input string: He didn't say, "Stop!"
+     * output string: He didn\'t say, \"Stop!\"
+     * 
+ *

+ * + * @param str String to escape values in, may be null + * @return String with escaped values, null if null string input + */ + public static String escapeJavaScript(String str) { + return escapeJavaStyleString(str, true, true); + } + + /** + *

Escapes the characters in a String using JavaScript String rules to a + * Writer.

+ * + *

A null string input has no effect.

+ * + * @param out Writer to write escaped string into + * @param str String to escape values in, may be null + * @throws IllegalArgumentException if the Writer is null + * @throws IOException if error occurs on underlying Writer + * @see #escapeJavaScript(java.lang.String) + */ + public static void escapeJavaScript(Writer out, String str) throws IOException { + escapeJavaStyleString(out, str, true, true); + } + + /** + *

Worker method for the {@link #escapeJavaScript(String)} method.

+ * + * @param str String to escape values in, may be null + * @param escapeSingleQuotes escapes single quotes if true + * @param escapeForwardSlash escapes forward slashes if true + * @return the escaped string + */ + private static String escapeJavaStyleString(String str, boolean escapeSingleQuotes, + boolean escapeForwardSlash) { + if (str == null) { + return null; + } + try { + StringWriter writer = new StringWriter(str.length() * 2); + escapeJavaStyleString(writer, str, escapeSingleQuotes, escapeForwardSlash); + return writer.toString(); + } catch (IOException ioe) { + // this should never ever happen while writing to a StringWriter + throw new RuntimeException(ioe); + } + } + + /** + *

Worker method for the {@link #escapeJavaScript(String)} method.

+ * + * @param out write to receieve the escaped string + * @param str String to escape values in, may be null + * @param escapeSingleQuote escapes single quotes if true + * @param escapeForwardSlash escapes forward slashes if true + * @throws IOException if an IOException occurs + */ + private static void escapeJavaStyleString(Writer out, String str, boolean escapeSingleQuote, + boolean escapeForwardSlash) throws IOException { + if (out == null) { + throw new IllegalArgumentException("The Writer must not be null"); + } + if (str == null) { + return; + } + int sz; + sz = str.length(); + for (int i = 0; i < sz; i++) { + char ch = str.charAt(i); + + // handle unicode + if (ch > 0xfff) { + out.write("\\u" + hex(ch)); + } else if (ch > 0xff) { + out.write("\\u0" + hex(ch)); + } else if (ch > 0x7f) { + out.write("\\u00" + hex(ch)); + } else if (ch < 32) { + switch (ch) { + case '\b': + out.write('\\'); + out.write('b'); + break; + case '\n': + out.write('\\'); + out.write('n'); + break; + case '\t': + out.write('\\'); + out.write('t'); + break; + case '\f': + out.write('\\'); + out.write('f'); + break; + case '\r': + out.write('\\'); + out.write('r'); + break; + default: + if (ch > 0xf) { + out.write("\\u00" + hex(ch)); + } else { + out.write("\\u000" + hex(ch)); + } + break; + } + } else { + switch (ch) { + case '\'': + if (escapeSingleQuote) { + out.write('\\'); + } + out.write('\''); + break; + case '"': + out.write('\\'); + out.write('"'); + break; + case '\\': + out.write('\\'); + out.write('\\'); + break; + case '/': + if (escapeForwardSlash) { + out.write('\\'); + } + out.write('/'); + break; + default: + out.write(ch); + break; + } + } + } + } + + /** + *

Returns an upper case hexadecimal String for the given character.

+ * + * @param ch The character to convert. + * @return An upper case hexadecimal String + */ + private static String hex(char ch) { + return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH); + } + + public static String join(String delimiter, String... stringParts) { + if (stringParts == null) { + return null; + } + String result = ""; + boolean notFirst = false; + for (String field : stringParts) { + if (notFirst) { + result += delimiter; + } + notFirst = true; + result += field; + } + return result; + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/VariousUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/VariousUtils.java new file mode 100644 index 000000000..985bd2b81 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/VariousUtils.java @@ -0,0 +1,34 @@ +package org.tomahawk.libtomahawk.utils; + +import java.io.File; +import java.io.FileNotFoundException; + +public class VariousUtils { + + public static final String TAG = VariousUtils.class.getSimpleName(); + + public static boolean containsIgnoreCase(String str1, String str2) { + return str1.toLowerCase().contains(str2.toLowerCase()); + } + + /** + * By default File#delete fails for non-empty directories, it works like "rm". We need something + * a little more brutal - this does the equivalent of "rm -r" + * + * @param path Root File Path + * @return true if the file and all sub files/directories have been removed + */ + public static boolean deleteRecursive(File path) throws FileNotFoundException { + if (!path.exists()) { + throw new FileNotFoundException(path.getAbsolutePath()); + } + boolean ret = true; + if (path.isDirectory()) { + for (File f : path.listFiles()) { + ret = ret && deleteRecursive(f); + } + } + return ret && path.delete(); + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/ViewUtils.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/ViewUtils.java new file mode 100644 index 000000000..12d3e3365 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/ViewUtils.java @@ -0,0 +1,106 @@ +package org.tomahawk.libtomahawk.utils; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewStub; +import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +public class ViewUtils { + + public static final String TAG = ViewUtils.class.getSimpleName(); + + public abstract static class ViewRunnable implements Runnable { + + private final View mView; + + public ViewRunnable(View view) { + this.mView = view; + } + + public View getLayedOutView() { + return mView; + } + } + + public static View ensureInflation(View view, int stubResId, int inflatedId) { + View stub = view.findViewById(stubResId); + if (stub instanceof ViewStub) { + return ((ViewStub) stub).inflate(); + } else { + return view.findViewById(inflatedId); + } + } + + /** + * This method converts dp unit to equivalent device specific value in pixels. + * + * @param dp A value in dp(Device independent pixels) unit. Which we need to convert into + * pixels + * @return A float value to represent Pixels equivalent to dp according to device + */ + public static int convertDpToPixel(int dp) { + Resources resources = TomahawkApp.getContext().getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return (int) (dp * (metrics.densityDpi / 160f)); + } + + /** + * Converts a track duration int into the proper String format + * + * @param duration the track's duration + * @return the formated string + */ + public static String durationToString(long duration) { + return String.format("%02d", (duration / 60000)) + ":" + String + .format("%02.0f", (double) (duration / 1000) % 60); + } + + public static void afterViewGlobalLayout(final ViewRunnable viewRunnable) { + if (viewRunnable.getLayedOutView().getHeight() > 0 + && viewRunnable.getLayedOutView().getWidth() > 0) { + viewRunnable.run(); + } else { + viewRunnable.getLayedOutView().getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + viewRunnable.run(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + viewRunnable.getLayedOutView().getViewTreeObserver() + .removeOnGlobalLayoutListener(this); + } else { + //noinspection deprecation + viewRunnable.getLayedOutView().getViewTreeObserver() + .removeGlobalOnLayoutListener(this); + } + } + }); + } + } + + public static void showSoftKeyboard(final EditText editText) { + editText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, final boolean hasFocus) { + editText.post(new Runnable() { + @Override + public void run() { + InputMethodManager imm = (InputMethodManager) TomahawkApp.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + } + }); + editText.setOnFocusChangeListener(null); + } + }); + editText.requestFocus(); + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfParser.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfParser.java new file mode 100644 index 000000000..0de249ae7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfParser.java @@ -0,0 +1,99 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils.parser; + +import com.squareup.okhttp.Response; + +import org.apache.commons.io.FileUtils; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.GsonXmlHelper; +import org.tomahawk.libtomahawk.utils.NetworkUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.net.Uri; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; + +public class XspfParser { + + private final static String TAG = XspfParser.class.getSimpleName(); + + public static Playlist parse(Uri uri) { + if (uri.getScheme().equals("file")) { + return parse(new File(uri.getPath())); + } else { + return parse(uri.toString()); + } + } + + public static Playlist parse(File file) { + String xspfString = null; + try { + xspfString = FileUtils.readFileToString(file, Charset.forName("UTF-8")); + } catch (IOException e) { + Log.e(TAG, "parse: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + return parseXspf(xspfString); + } + + public static Playlist parse(String url) { + String xspfString = null; + Response response = null; + try { + response = NetworkUtils.httpRequest(null, url, null, null, null, null, true, null); + xspfString = response.body().string(); + } catch (IOException e) { + Log.e(TAG, "parse: " + e.getClass() + ": " + e.getLocalizedMessage()); + } finally { + if (response != null) { + try { + response.body().close(); + } catch (IOException e) { + Log.e(TAG, "parse: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + } + return parseXspf(xspfString); + } + + public static Playlist parseXspf(String xspfString) { + if (xspfString != null) { + XspfPlaylist xspfPlaylist = + GsonXmlHelper.get().fromXml(xspfString, XspfPlaylist.class); + if (xspfPlaylist != null && xspfPlaylist.trackList != null) { + ArrayList qs = new ArrayList<>(); + for (XspfPlaylistTrack track : xspfPlaylist.trackList) { + qs.add(Query.get(track.title, track.album, track.creator, false)); + } + String title = xspfPlaylist.title == null ? "XSPF Playlist" : xspfPlaylist.title; + Playlist pl = Playlist + .fromQueryList(IdGenerator.getLifetimeUniqueStringId(), title, null, qs); + pl.setFilled(true); + return pl; + } + } + Log.e(TAG, "parse: couldn't read xspf playlist"); + return null; + } + +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylist.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylist.java new file mode 100644 index 000000000..c3bdaad09 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylist.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils.parser; + +import java.util.List; + +public class XspfPlaylist { + + public String title; + + public String creator; + + public String info; + + public String location; + + public String date; + + public List trackList; + + public XspfPlaylist() { + } +} diff --git a/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylistTrack.java b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylistTrack.java new file mode 100644 index 000000000..c1e037294 --- /dev/null +++ b/app/src/main/java/org/tomahawk/libtomahawk/utils/parser/XspfPlaylistTrack.java @@ -0,0 +1,36 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.libtomahawk.utils.parser; + +public class XspfPlaylistTrack { + + public String title; + + public String creator; + + public String album; + + public String location; + + public String info; + + public String duration; + + public XspfPlaylistTrack() { + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/TomahawkApp.java b/app/src/main/java/org/tomahawk/tomahawk_android/TomahawkApp.java new file mode 100644 index 000000000..e9fd6b28e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/TomahawkApp.java @@ -0,0 +1,104 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android; + +import org.acra.ACRA; +import org.acra.ReportingInteractionMode; +import org.acra.annotation.ReportsCrashes; +import org.acra.sender.HttpSender; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.TomahawkHttpSender; + +import android.app.Application; +import android.content.Context; +import android.os.StrictMode; +import android.util.Log; + +/** + * This class represents the Application core. + */ +@ReportsCrashes( + httpMethod = HttpSender.Method.PUT, + reportType = HttpSender.Type.JSON, + formUri = "http://crash-stats.tomahawk-player.org:5984/acra-tomahawkandroid/_design/acra-storage/_update/report", + formUriBasicAuthLogin = "reporter", + formUriBasicAuthPassword = "unknackbar", + excludeMatchingSharedPreferencesKeys = {".*_config$"}, + mode = ReportingInteractionMode.DIALOG, + logcatArguments = {"-t", "2000", "-v", "time"}, + resDialogText = R.string.crash_dialog_text, + resDialogIcon = android.R.drawable.ic_dialog_info, + resDialogTitle = R.string.crash_dialog_title, + resDialogCommentPrompt = R.string.crash_dialog_comment_prompt, + resDialogOkToast = R.string.crash_dialog_ok_toast) +public class TomahawkApp extends Application { + + private static final String TAG = TomahawkApp.class.getSimpleName(); + + public final static String PLUGINNAME_HATCHET = "hatchet"; + + public final static String PLUGINNAME_USERCOLLECTION = "usercollection"; + + public final static String PLUGINNAME_SPOTIFY = "spotify"; + + public final static String PLUGINNAME_DEEZER = "deezer"; + + public final static String PLUGINNAME_BEATSMUSIC = "beatsmusic"; + + public final static String PLUGINNAME_JAMENDO = "jamendo"; + + public final static String PLUGINNAME_OFFICIALFM = "officialfm"; + + public final static String PLUGINNAME_SOUNDCLOUD = "soundcloud"; + + public final static String PLUGINNAME_GMUSIC = "gmusic"; + + public final static String PLUGINNAME_AMZN = "amazon"; + + private static Context sApplicationContext; + + @Override + public void onCreate() { + ACRA.init(this); + ACRA.getErrorReporter().setReportSender( + new TomahawkHttpSender(ACRA.getConfig().httpMethod(), ACRA.getConfig().reportType(), + null)); + + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectCustomSlowCalls().detectDiskReads() + .detectDiskWrites().detectNetwork().penaltyLog().build()); + try { + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .setClassInstanceLimit(Class.forName(PlaybackService.class.getName()), 1) + .penaltyLog().build()); + } catch (ClassNotFoundException e) { + Log.e(TAG, e.toString()); + } + + super.onCreate(); + + sApplicationContext = getApplicationContext(); + } + + public static Context getContext() { + return sApplicationContext; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/activities/TomahawkMainActivity.java b/app/src/main/java/org/tomahawk/tomahawk_android/activities/TomahawkMainActivity.java new file mode 100755 index 000000000..f75575faa --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/activities/TomahawkMainActivity.java @@ -0,0 +1,1079 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.activities; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; +import com.uservoice.uservoicesdk.Config; +import com.uservoice.uservoicesdk.UserVoice; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.DbCollection; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.database.TomahawkSQLiteHelper; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Result; +import org.tomahawk.libtomahawk.resolver.UserCollectionStubResolver; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.libtomahawk.utils.parser.XspfParser; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.adapters.SuggestionSimpleCursorAdapter; +import org.tomahawk.tomahawk_android.dialogs.GMusicConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.InstallPluginConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.WarnOldPluginDialog; +import org.tomahawk.tomahawk_android.fragments.ArtistPagerFragment; +import org.tomahawk.tomahawk_android.fragments.ContentHeaderFragment; +import org.tomahawk.tomahawk_android.fragments.ContextMenuFragment; +import org.tomahawk.tomahawk_android.fragments.PlaylistEntriesFragment; +import org.tomahawk.tomahawk_android.fragments.PreferencePagerFragment; +import org.tomahawk.tomahawk_android.fragments.SearchPagerFragment; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.fragments.WelcomeFragment; +import org.tomahawk.tomahawk_android.listeners.TomahawkPanelSlideListener; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.MediaPlayIntentHandler; +import org.tomahawk.tomahawk_android.utils.MenuDrawer; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.utils.PluginUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.utils.SearchViewStyle; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; +import org.tomahawk.tomahawk_android.views.PlaybackPanel; + +import android.accounts.AccountManager; +import android.app.SearchManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.Cursor; +import android.database.sqlite.SQLiteCursor; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.v4.app.FragmentManager; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import de.greenrobot.event.EventBus; +import fr.castorflex.android.smoothprogressbar.SmoothProgressBar; + +/** + * The main Tomahawk activity + */ +public class TomahawkMainActivity extends AppCompatActivity { + + private final static String TAG = TomahawkMainActivity.class.getSimpleName(); + + public static final String SAVED_PLAYBACK_STATE = "saved_playback_state"; + + public static final String SHOW_PLAYBACKFRAGMENT_ON_STARTUP + = "show_playbackfragment_on_startup"; + + protected final HashSet mCorrespondingRequestIds = new HashSet<>(); + + private MediaBrowserCompat mMediaBrowser; + + private int mPlaybackState = PlaybackStateCompat.STATE_NONE; + + private final MediaBrowserCompat.ConnectionCallback mConnectionCallback = + new MediaBrowserCompat.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "MediaBrowser connected"); + try { + MediaControllerCompat mediaController = new MediaControllerCompat( + TomahawkMainActivity.this, mMediaBrowser.getSessionToken()); + setSupportMediaController(mediaController); + mediaController.registerCallback(mMediaCallback); + mPlaybackPanel.setMediaController(mediaController); + mMediaCallback.onPlaybackStateChanged(mediaController.getPlaybackState()); + ContentHeaderFragment.MediaControllerConnectedEvent event + = new ContentHeaderFragment.MediaControllerConnectedEvent(); + EventBus.getDefault().post(event); + } catch (RemoteException e) { + Log.e(TAG, "Could not connect media controller: ", e); + } + } + }; + + private final MediaControllerCompat.Callback mMediaCallback + = new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { + Log.d(TAG, "onPlaybackstate changed" + state); + mPlaybackState = state.getState(); + mPlaybackPanel.updatePlaybackState(state); + if (getSupportMediaController() != null) { + String playbackManagerId = getSupportMediaController().getExtras().getString( + PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + PlaybackManager playbackManager = PlaybackManager.getByKey(playbackManagerId); + if (playbackManager != null && (playbackManager.getCurrentEntry() != null + || playbackManager.getPlaylist() instanceof StationPlaylist)) { + showPanel(); + } else { + hidePanel(); + } + } else { + hidePanel(); + } + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + if (metadata != null) { + Log.d(TAG, "onMetadataChanged changed" + metadata); + mPlaybackPanel.updateMetadata(metadata); + } + } + + @Override + public void onQueueChanged(List queue) { + } + }; + + private MenuItem mSearchItem; + + private MenuDrawer mMenuDrawer; + + private ActionBarDrawerToggle mDrawerToggle; + + private CharSequence mTitle; + + private CharSequence mDrawerTitle; + + private TomahawkMainReceiver mTomahawkMainReceiver; + + private SmoothProgressBar mSmoothProgressBar; + + private SlidingUpPanelLayout mSlidingUpPanelLayout; + + private TomahawkPanelSlideListener mPanelSlideListener; + + private PlaybackPanel mPlaybackPanel; + + private View mActionBarBg; + + private boolean mDestroyed; + + private Handler mShouldShowAnimationHandler; + + private final Runnable mShouldShowAnimationRunnable = new Runnable() { + @Override + public void run() { + if (ThreadManager.get().isActive() + || mPlaybackState == PlaybackStateCompat.STATE_BUFFERING + || CollectionManager.get().getUserCollection().isWorking()) { + mSmoothProgressBar.setVisibility(View.VISIBLE); + } else { + mSmoothProgressBar.setVisibility(View.GONE); + } + mShouldShowAnimationHandler.postDelayed(mShouldShowAnimationRunnable, 500); + } + }; + + public static class ShowWebViewEvent { + + public int mRequestid; + + public String mUrl; + } + + /** + * Handles incoming broadcasts. + */ + private class TomahawkMainReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + boolean noConnectivity = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + if (!noConnectivity) { + AuthenticatorUtils hatchetAuthUtils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendLoggedOps(hatchetAuthUtils); + } + } + } + } + + /** + * If the {@link PipeLine} was able to parse a given url (like a link to a spotify track for + * example), then this method receives the result object. + * + * @param event the result object which contains the parsed data + */ + @SuppressWarnings("unused") + public void onEventAsync(PipeLine.UrlResultsEvent event) { + final Bundle bundle = new Bundle(); + List queries; + Query query; + Playlist playlist; + switch (event.mResult.type) { + case PipeLine.URL_TYPE_ARTIST: + bundle.putString(TomahawkFragment.ARTIST, + Artist.get(event.mResult.artist).getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC_PAGER); + bundle.putLong(TomahawkFragment.CONTAINER_FRAGMENT_ID, + IdGenerator.getSessionUniqueId()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(TomahawkMainActivity.this, ArtistPagerFragment.class, + bundle); + } + }); + break; + case PipeLine.URL_TYPE_ALBUM: + Artist artist = Artist.get(event.mResult.artist); + bundle.putString(TomahawkFragment.ALBUM, + Album.get(event.mResult.album, artist).getCacheKey()); + bundle.putString( + TomahawkFragment.COLLECTION_ID, TomahawkApp.PLUGINNAME_HATCHET); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace( + TomahawkMainActivity.this, PlaylistEntriesFragment.class, bundle); + } + }); + break; + case PipeLine.URL_TYPE_TRACK: + queries = new ArrayList<>(); + query = Query.get(event.mResult.track, "", event.mResult.artist, false); + queries.add(query); + playlist = Playlist.fromQueryList(IdGenerator.getSessionUniqueStringId(), + event.mResult.track + " - " + event.mResult.artist, "", queries); + playlist.setFilled(true); + bundle.putString(TomahawkFragment.PLAYLIST, playlist.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace( + TomahawkMainActivity.this, PlaylistEntriesFragment.class, bundle); + } + }); + break; + case PipeLine.URL_TYPE_PLAYLIST: + queries = new ArrayList<>(); + for (ScriptResolverUrlResult track : event.mResult.tracks) { + query = Query.get(track.track, "", track.artist, false); + if (event.mResolver != null && event.mResolver.isEnabled() + && track.hint != null) { + Result result = Result.get(track.hint, query.getBasicTrack(), + event.mResolver); + float trackScore = query.howSimilar(result); + query.addTrackResult(result, trackScore); + } + queries.add(query); + } + playlist = Playlist.fromQueryList(IdGenerator.getLifetimeUniqueStringId(), + event.mResult.title, null, queries); + playlist.setFilled(true); + bundle.putString(TomahawkFragment.PLAYLIST, playlist.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(TomahawkMainActivity.this, + PlaylistEntriesFragment.class, bundle); + } + }); + break; + case PipeLine.URL_TYPE_XSPFURL: + Playlist pl = XspfParser.parse(event.mResult.url); + if (pl != null) { + bundle.putString(TomahawkFragment.PLAYLIST, pl.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(TomahawkMainActivity.this, + PlaylistEntriesFragment.class, bundle); + } + }); + } + break; + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(DbCollection.InitializedEvent event) { + if (mMenuDrawer != null) { + mMenuDrawer.updateDrawer(this); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + if (event.mInfoRequestData != null + && event.mInfoRequestData.getType() + == InfoRequestData.INFOREQUESTDATA_TYPE_USERS + && mMenuDrawer != null) { + mMenuDrawer.updateDrawer(this); + } + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(CollectionManager.AddedOrRemovedEvent event) { + if (mMenuDrawer != null) { + mMenuDrawer.updateDrawer(this); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(HatchetAuthenticatorUtils.UserLoginEvent event) { + if (mMenuDrawer != null) { + mMenuDrawer.updateDrawer(this); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(AuthenticatorManager.ConfigTestResultEvent event) { + if (event.mComponent instanceof HatchetAuthenticatorUtils + && (event.mType == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS + || event.mType == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_LOGOUT)) { + onHatchetLoggedInOut( + event.mType == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(ShowWebViewEvent event) { + Intent intent = new Intent(this, WebViewActivity.class); + intent.putExtra(WebViewActivity.URL_EXTRA, event.mUrl); + intent.putExtra(WebViewActivity.REQUESTID_EXTRA, event.mRequestid); + startActivity(intent); + } + + @SuppressWarnings("unused") + public void onEventAsync(PipeLine.ResolversChangedEvent event) { + String resolverId = event.mScriptResolver.getId(); + if ((resolverId.equals(TomahawkApp.PLUGINNAME_DEEZER) + || resolverId.equals(TomahawkApp.PLUGINNAME_SPOTIFY)) + && event.mScriptResolver.isEnabled() + && !PluginUtils.isPluginUpToDate(resolverId)) { + PipeLine.get().getResolver(resolverId).setEnabled(false); + WarnOldPluginDialog dialog = new WarnOldPluginDialog(); + Bundle args = new Bundle(); + args.putString(TomahawkFragment.PREFERENCEID, resolverId); + if (PluginUtils.isPluginInstalled(resolverId)) { + args.putString(TomahawkFragment.MESSAGE, getString(R.string.warn_old_plugin)); + } else { + args.putString(TomahawkFragment.MESSAGE, getString(R.string.warn_no_plugin)); + } + dialog.setArguments(args); + dialog.show(getSupportFragmentManager(), null); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + GMusicConfigDialog.ActivityResultEvent event = new GMusicConfigDialog.ActivityResultEvent(); + event.resultCode = resultCode; + event.requestCode = requestCode; + EventBus.getDefault().post(event); + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mPlaybackState = savedInstanceState.getInt(SAVED_PLAYBACK_STATE); + } + + PipeLine.get(); + + CollectionManager.get().getUserCollection().loadMediaItems(false); + + setContentView(R.layout.tomahawk_main_activity); + + mMediaBrowser = new MediaBrowserCompat(this, + new ComponentName(this, PlaybackService.class), mConnectionCallback, null); + + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + mSmoothProgressBar = (SmoothProgressBar) findViewById(R.id.smoothprogressbar); + + mTitle = mDrawerTitle = getTitle().toString().toUpperCase(); + getSupportActionBar().setTitle(""); + + mPlaybackPanel = (PlaybackPanel) findViewById(R.id.playback_panel); + + mSlidingUpPanelLayout = (SlidingUpPanelLayout) findViewById(R.id.sliding_layout); + FragmentUtils.addPlaybackFragment(this); + mPanelSlideListener = + new TomahawkPanelSlideListener(this, mSlidingUpPanelLayout, mPlaybackPanel); + mSlidingUpPanelLayout.setPanelSlideListener(mPanelSlideListener); + if (mPlaybackState != PlaybackStateCompat.STATE_NONE) { + showPanel(); + } else { + hidePanel(); + } + + mActionBarBg = findViewById(R.id.action_bar_background); + + mMenuDrawer = (MenuDrawer) findViewById(R.id.drawer_layout); + if (mMenuDrawer != null) { + mDrawerToggle = new ActionBarDrawerToggle(this, mMenuDrawer, R.string.drawer_open, + R.string.drawer_close) { + + /** Called when a drawer has settled in a completely closed state. */ + public void onDrawerClosed(View view) { + getSupportActionBar().setTitle(mTitle); + } + + /** Called when a drawer has settled in a completely open state. */ + public void onDrawerOpened(View drawerView) { + getSupportActionBar().setTitle(mDrawerTitle); + if (mSearchItem != null) { + MenuItemCompat.collapseActionView(mSearchItem); + } + } + }; + // Set the drawer toggle as the DrawerListener + mMenuDrawer.addDrawerListener(mDrawerToggle); + } + + // set customization variables on the ActionBar + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayShowCustomEnabled(true); + + //Setup UserVoice + Config config = new Config("tomahawk.uservoice.com"); + config.setForumId(224204); + config.setTopicId(62613); + UserVoice.init(config, TomahawkMainActivity.this); + + //Resolve currently logged-in user + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolve(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + + //Ask for notification service access if hatchet user logged in + PreferenceUtils.attemptAskAccess(this); + + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(final User user) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!mDestroyed) { + FragmentUtils.addRootFragment(TomahawkMainActivity.this, user); + + if (!PreferenceUtils.getBoolean( + PreferenceUtils.COACHMARK_WELCOMEFRAGMENT_DISABLED)) { + FragmentUtils.replace(TomahawkMainActivity.this, + WelcomeFragment.class, null); + } + } + } + }); + } + }); + + handleIntent(getIntent()); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + // Sync the toggle state after onRestoreInstanceState has occurred. + if (mDrawerToggle != null) { + mDrawerToggle.syncState(); + } + } + + @Override + protected void onNewIntent(final Intent intent) { + super.onNewIntent(intent); + + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + if (MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH.equals(intent.getAction())) { + intent.setAction(null); + String playbackManagerId = getSupportMediaController().getExtras().getString( + PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + PlaybackManager playbackManager = PlaybackManager.getByKey(playbackManagerId); + MediaPlayIntentHandler intentHandler = new MediaPlayIntentHandler( + getSupportMediaController().getTransportControls(), playbackManager); + intentHandler.mediaPlayFromSearch(intent.getExtras()); + } + if ("com.google.android.gms.actions.SEARCH_ACTION".equals(intent.getAction())) { + intent.setAction(null); + String query = intent.getStringExtra(SearchManager.QUERY); + if (query != null && !query.isEmpty()) { + DatabaseHelper.get().addEntryToSearchHistory(query); + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.QUERY_STRING, query); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + FragmentUtils.replace(TomahawkMainActivity.this, SearchPagerFragment.class, bundle); + } + } + if (SHOW_PLAYBACKFRAGMENT_ON_STARTUP.equals(intent.getAction())) { + intent.setAction(null); + // if this Activity is being shown after the user clicked the notification + if (mSlidingUpPanelLayout != null) { + mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED); + } + } + if (intent.hasExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)) { + intent.removeExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + Bundle bundle = new Bundle(); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_SMALL); + FragmentUtils.replace(this, PreferencePagerFragment.class, bundle); + } + + if (intent.getData() != null) { + final Uri data = intent.getData(); + intent.setData(null); + List pathSegments = data.getPathSegments(); + String host = data.getHost(); + String scheme = data.getScheme(); + if ((scheme != null && (scheme.equals("spotify") || scheme.equals("tomahawk"))) + || (host != null && (host.contains("spotify.com") || host.contains("hatchet.is") + || host.contains("toma.hk") || host.contains("beatsmusic.com") + || host.contains("deezer.com") || host.contains("rdio.com") + || host.contains("soundcloud.com")))) { + PipeLine.get().lookupUrl(data.toString()); + } else if ((pathSegments != null + && pathSegments.get(pathSegments.size() - 1).endsWith(".xspf")) + || (intent.getType() != null + && intent.getType().equals("application/xspf+xml"))) { + TomahawkRunnable r = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_HIGH) { + @Override + public void run() { + Playlist pl = XspfParser.parse(data); + if (pl != null) { + final Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.PLAYLIST, pl.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(TomahawkMainActivity.this, + PlaylistEntriesFragment.class, bundle); + } + }); + } + } + }; + ThreadManager.get().execute(r); + } else if (pathSegments != null + && (pathSegments.get(pathSegments.size() - 1).endsWith(".axe") + || pathSegments.get(pathSegments.size() - 1).endsWith(".AXE"))) { + InstallPluginConfigDialog dialog = new InstallPluginConfigDialog(); + Bundle args = new Bundle(); + args.putString(InstallPluginConfigDialog.PATH_TO_AXE_URI_STRING, data.toString()); + dialog.setArguments(args); + dialog.show(getSupportFragmentManager(), null); + } else { + String albumName; + String trackName; + String artistName; + try { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(this, data); + albumName = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); + artistName = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); + trackName = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + retriever.release(); + } catch (Exception e) { + Log.e(TAG, "handleIntent: " + e.getClass() + ": " + e.getLocalizedMessage()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + String msg = TomahawkApp.getContext().getString(R.string.invalid_file); + Toast.makeText(TomahawkApp.getContext(), msg, Toast.LENGTH_LONG).show(); + } + }); + return; + } + if (TextUtils.isEmpty(trackName) && pathSegments != null) { + trackName = pathSegments.get(pathSegments.size() - 1); + } + Query query = Query.get(trackName, albumName, artistName, false); + Result result = Result.get(data.toString(), query.getBasicTrack(), + UserCollectionStubResolver.get()); + float trackScore = query.howSimilar(result); + query.addTrackResult(result, trackScore); + Bundle bundle = new Bundle(); + List queries = new ArrayList<>(); + queries.add(query); + Playlist playlist = Playlist.fromQueryList( + IdGenerator.getSessionUniqueStringId(), "", "", queries); + playlist.setFilled(true); + playlist.setName(artistName + " - " + trackName); + bundle.putString(TomahawkFragment.PLAYLIST, playlist.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace( + TomahawkMainActivity.this, PlaylistEntriesFragment.class, bundle); + } + } + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + + if (mMediaBrowser != null) { + mMediaBrowser.connect(); + } + } + + @Override + public void onResume() { + super.onResume(); + + mMenuDrawer.updateDrawer(this); + + if (mSlidingUpPanelLayout.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) { + mPlaybackPanel.setVisibility(View.GONE); + } else { + mPlaybackPanel.setup(mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.EXPANDED); + mPlaybackPanel.setVisibility(View.VISIBLE); + if (mSlidingUpPanelLayout.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + mPanelSlideListener.onPanelSlide(mSlidingUpPanelLayout, 1f); + } else { + mPanelSlideListener.onPanelSlide(mSlidingUpPanelLayout, 0f); + } + } + + if (mShouldShowAnimationHandler == null) { + mShouldShowAnimationHandler = new Handler(); + mShouldShowAnimationHandler.post(mShouldShowAnimationRunnable); + } + + if (mTomahawkMainReceiver == null) { + mTomahawkMainReceiver = new TomahawkMainReceiver(); + } + + // Register intents that the BroadcastReceiver should listen to + registerReceiver(mTomahawkMainReceiver, + new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + // Install listener that disables the navigation drawer and hides the actionbar whenever + // a WelcomeFragment or ContextMenuFragment is the currently shown Fragment. + getSupportFragmentManager().addOnBackStackChangedListener( + new FragmentManager.OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + updateActionBarState(true); + } + }); + updateActionBarState(true); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + + if (mMediaBrowser != null) { + mMediaBrowser.disconnect(); + } + if (getSupportMediaController() != null) { + getSupportMediaController().unregisterCallback(mMediaCallback); + } + + super.onStop(); + } + + @Override + public void onPause() { + super.onPause(); + + if (mShouldShowAnimationHandler != null) { + mShouldShowAnimationHandler.removeCallbacks(mShouldShowAnimationRunnable); + mShouldShowAnimationHandler = null; + } + + if (mTomahawkMainReceiver != null) { + unregisterReceiver(mTomahawkMainReceiver); + mTomahawkMainReceiver = null; + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putInt(SAVED_PLAYBACK_STATE, mPlaybackState); + + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onDestroy() { + mDestroyed = true; + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + if (mDrawerToggle != null) { + mDrawerToggle.onConfigurationChanged(newConfig); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.clear(); + getMenuInflater().inflate(R.menu.tomahawk_main_menu, menu); + + // customize the searchView + mSearchItem = menu.findItem(R.id.action_search); + final SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem); + SearchViewStyle.on(searchView) + .setSearchPlateDrawableId(R.drawable.edittext_background) + .setCursorColor(getResources().getColor(R.color.tomahawk_red)); + searchView.setQueryHint(getString(R.string.search)); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (query != null && !TextUtils.isEmpty(query)) { + DatabaseHelper.get().addEntryToSearchHistory(query); + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.QUERY_STRING, query); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + FragmentUtils + .replace(TomahawkMainActivity.this, SearchPagerFragment.class, bundle); + if (mSearchItem != null) { + MenuItemCompat.collapseActionView(mSearchItem); + } + searchView.clearFocus(); + return true; + } + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + Cursor cursor = DatabaseHelper.get().getSearchHistoryCursor(newText); + if (cursor.getCount() != 0) { + String[] columns = new String[]{ + TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY}; + int[] columnTextId = new int[]{android.R.id.text1}; + + SuggestionSimpleCursorAdapter simple = new SuggestionSimpleCursorAdapter( + getBaseContext(), R.layout.searchview_dropdown_item, + cursor, columns, columnTextId, 0); + + if (searchView.getSuggestionsAdapter() != null + && searchView.getSuggestionsAdapter().getCursor() != null) { + searchView.getSuggestionsAdapter().getCursor().close(); + } + searchView.setSuggestionsAdapter(simple); + return true; + } else { + cursor.close(); + return false; + } + } + }); + searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + SQLiteCursor cursor = (SQLiteCursor) searchView.getSuggestionsAdapter() + .getItem(position); + int indexColumnSuggestion = cursor + .getColumnIndex(TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY); + + searchView.setQuery(cursor.getString(indexColumnSuggestion), false); + + return true; + } + + @Override + public boolean onSuggestionClick(int position) { + SQLiteCursor cursor = (SQLiteCursor) searchView.getSuggestionsAdapter() + .getItem(position); + int indexColumnSuggestion = cursor + .getColumnIndex(TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY); + + searchView.setQuery(cursor.getString(indexColumnSuggestion), false); + + return true; + } + }); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Pass the event to ActionBarDrawerToggle, if it returns + // true, then it has handled the app icon touch event + return mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item) || + super.onOptionsItemSelected(item); + } + + @Override + public void setTitle(CharSequence title) { + mTitle = title; + getSupportActionBar().setTitle(mTitle); + } + + public void closeDrawer() { + if (mMenuDrawer != null) { + mMenuDrawer.closeDrawer(); + } + } + + public void onHatchetLoggedInOut(boolean loggedIn) { + if (loggedIn) { + PreferenceUtils.attemptAskAccess(this); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolve(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } + if (mMenuDrawer != null) { + mMenuDrawer.updateDrawer(this); + } + } + + @Override + public void onBackPressed() { + if (findViewById(R.id.context_menu_fragment) == null && mSlidingUpPanelLayout.isEnabled() + && (mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.EXPANDED + || mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.ANCHORED)) { + mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } else { + if (mSlidingUpPanelLayout.getPanelState() != SlidingUpPanelLayout.PanelState.HIDDEN) { + AnimationUtils.fade(mPlaybackPanel, AnimationUtils.DURATION_CONTEXTMENU, true); + } + super.onBackPressed(); + } + } + + public float getSlidingOffset() { + return mPanelSlideListener.getSlidingOffset(); + } + + public void collapsePanel() { + if (mSlidingUpPanelLayout.getPanelState() != SlidingUpPanelLayout.PanelState.HIDDEN) { + mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + } + + public void showPanel() { + if (mSlidingUpPanelLayout.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) { + mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + mPlaybackPanel.setup(mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.EXPANDED); + showPlaybackPanel(true); + } + } + + public void hidePanel() { + if (mSlidingUpPanelLayout.getPanelState() != SlidingUpPanelLayout.PanelState.HIDDEN) { + mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN); + hidePlaybackPanel(); + } + } + + /** + * Disables the navigation drawer and hides the actionbar whenever WelcomeFragment or + * ContextMenuFragment is the currently shown Fragment or if the playback {@link + * SlidingUpPanelLayout} is more than half way expanded. + * + * @param checkCurrentFragment whether or not to check the current Fragment. This can be set to + * false to improve performance when this function is repeatedly + * being called in {@link org.tomahawk.tomahawk_android.listeners.TomahawkPanelSlideListener} + */ + public void updateActionBarState(boolean checkCurrentFragment) { + boolean hideActionBar = mPanelSlideListener.getSlidingOffset() > 0.5f; + boolean forced = true; + if (checkCurrentFragment && !hideActionBar) { + forced = false; + int size = getSupportFragmentManager().getBackStackEntryCount(); + if (size > 0) { + String clssName = + getSupportFragmentManager().getBackStackEntryAt(size - 1).getName(); + hideActionBar = WelcomeFragment.class.getName().equals(clssName) + || ContextMenuFragment.class.getName().equals(clssName); + } + } + if (hideActionBar) { + if (mMenuDrawer != null) { + mMenuDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + } + hideActionbar(); + } else { + if (mMenuDrawer != null) { + mMenuDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); + } + showActionBar(forced); + } + } + + private void showActionBar(boolean forced) { + if (forced || mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.COLLAPSED + || mSlidingUpPanelLayout.getPanelState() + == SlidingUpPanelLayout.PanelState.HIDDEN) { + if (!getSupportActionBar().isShowing()) { + getSupportActionBar().show(); + } + int actionBarHeight = getResources().getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material); + AnimationUtils.moveY(mActionBarBg, 0, -actionBarHeight, 250, true); + } + } + + private void hideActionbar() { + if (getSupportActionBar().isShowing()) { + getSupportActionBar().hide(); + } + int actionBarHeight = getResources().getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material); + AnimationUtils.moveY(mActionBarBg, 0, -actionBarHeight, 250, false); + } + + public void showPlaybackPanel(boolean forced) { + if (forced || mSlidingUpPanelLayout.getPanelState() + != SlidingUpPanelLayout.PanelState.HIDDEN) { + AnimationUtils.fade(mPlaybackPanel, AnimationUtils.DURATION_CONTEXTMENU, true); + if (!PreferenceUtils.getBoolean(PreferenceUtils.COACHMARK_SEEK_DISABLED) + && PreferenceUtils.getLong(PreferenceUtils.COACHMARK_SEEK_TIMESTAMP) + 259200000 + < System.currentTimeMillis()) { + final View coachMark = ViewUtils.ensureInflation(mPlaybackPanel, + R.id.playbackpanel_seek_coachmark_stub, R.id.playbackpanel_seek_coachmark); + coachMark.findViewById(R.id.close_button).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + coachMark.setVisibility(View.GONE); + } + }); + coachMark.setVisibility(View.VISIBLE); + PreferenceUtils.edit().putLong(PreferenceUtils.COACHMARK_SEEK_TIMESTAMP, + System.currentTimeMillis()).apply(); + } + } + } + + public void hidePlaybackPanel() { + AnimationUtils.fade(mPlaybackPanel, AnimationUtils.DURATION_CONTEXTMENU, false); + } + + public PlaybackPanel getPlaybackPanel() { + return mPlaybackPanel; + } + + public SlidingUpPanelLayout getSlidingUpPanelLayout() { + return mSlidingUpPanelLayout; + } + + public void showFilledActionBar() { + findViewById(R.id.action_bar_background).setBackgroundResource( + R.color.primary_background_inverted); + } + + public void showGradientActionBar() { + findViewById(R.id.action_bar_background).setBackgroundResource(R.drawable.below_shadow); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/activities/WebViewActivity.java b/app/src/main/java/org/tomahawk/tomahawk_android/activities/WebViewActivity.java new file mode 100755 index 000000000..e5f0b2a89 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/activities/WebViewActivity.java @@ -0,0 +1,91 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.activities; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.os.Bundle; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import de.greenrobot.event.EventBus; + +public class WebViewActivity extends Activity { + + public static final String TAG = WebViewActivity.class.getSimpleName(); + + public static final String URL_EXTRA = "url"; + + public static final String REQUESTID_EXTRA = "requestId"; + + private WebView mWebView; + + @SuppressLint("SetJavaScriptEnabled") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.web_view_activity); + + String url = getIntent().getStringExtra(URL_EXTRA); + final int requestId = getIntent().getIntExtra(REQUESTID_EXTRA, -1); + + mWebView = (WebView) findViewById(R.id.webview); + mWebView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + ScriptAccount account; + if (url.startsWith("tomahawkspotifyresolver")) { + account = PipeLine.get().getResolver(TomahawkApp.PLUGINNAME_SPOTIFY) + .getScriptAccount(); + } else if (url.startsWith("tomahawkdeezerresolver")) { + account = PipeLine.get().getResolver(TomahawkApp.PLUGINNAME_DEEZER) + .getScriptAccount(); + } else { + view.loadUrl(url); + return false; + } + account.onShowWebViewFinished(requestId, url); + finish(); + return true; + } + }); + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.loadUrl(url); + } + + @Override + public void onBackPressed() { + if (mWebView.canGoBack()) { + mWebView.goBack(); + } else { + AuthenticatorManager.ConfigTestResultEvent event + = new AuthenticatorManager.ConfigTestResultEvent(); + event.mComponent = PipeLine.get() + .getResolver(TomahawkApp.PLUGINNAME_SPOTIFY); + EventBus.getDefault().post(event); + super.onBackPressed(); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/AlbumArtSwipeAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/AlbumArtSwipeAdapter.java new file mode 100755 index 000000000..6f240d992 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/AlbumArtSwipeAdapter.java @@ -0,0 +1,270 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; + +import android.content.res.Configuration; +import android.os.Parcelable; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +/** + * {@link PagerAdapter} which provides functionality to swipe an AlbumArt image. Used in {@link + * org.tomahawk.tomahawk_android.fragments.PlaybackFragment} + */ +public class AlbumArtSwipeAdapter extends PagerAdapter { + + private static final String TAG = AlbumArtSwipeAdapter.class.getSimpleName(); + + //Used to provide fake infinite swiping behaviour, if current Playlist is repeating + private static final int FAKE_INFINITY_COUNT = 20000; + + private int mFakeInfinityOffset; + + private final ViewPager mViewPager; + + private int mLastItem; + + private MediaControllerCompat mMediaController; + + private PlaybackManager mPlaybackManager; + + private int mSize = 0; + + private int mRepeatMode = PlaybackManager.NOT_REPEATING; + + private int mPageScrollState = ViewPager.SCROLL_STATE_IDLE; + + private boolean mUpdateWhenIdle = false; + + private final ViewPager.OnPageChangeListener mOnPageChangeListener + = new ViewPager.OnPageChangeListener() { + + @Override + public void onPageSelected(int position) { + if (mMediaController != null) { + if (position == mLastItem - 1) { + Log.d(TAG, "Selected page is now " + position + ", was " + mLastItem + + ". Skipping to previous track."); + mMediaController.getTransportControls().skipToPrevious(); + } else if (position == mLastItem + 1) { + Log.d(TAG, "Selected page is now " + position + ", was " + mLastItem + + ". Skipping to next track."); + mMediaController.getTransportControls().skipToNext(); + } + } else { + Log.e(TAG, "Couldn't skip to next/previous track. mMediaController is null"); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageScrollStateChanged(int state) { + int lastState = mPageScrollState; + mPageScrollState = state; + if (mUpdateWhenIdle && lastState != ViewPager.SCROLL_STATE_IDLE + && state == ViewPager.SCROLL_STATE_IDLE) { + mUpdateWhenIdle = false; + updatePlaylist(); + } + } + }; + + /** + * Constructs a new AlbumArtSwipeAdapter. + * + * @param viewPager ViewPager which this adapter has been connected with + */ + public AlbumArtSwipeAdapter(ViewPager viewPager) { + mViewPager = viewPager; + mViewPager.addOnPageChangeListener(mOnPageChangeListener); + } + + public void setMediaController(MediaControllerCompat mediaController) { + mMediaController = mediaController; + } + + public void setPlaybackManager(PlaybackManager playbackManager) { + mPlaybackManager = playbackManager; + } + + /** + * Instantiate an item in the {@link PagerAdapter}. Fill it async with the correct AlbumArt + * image. + */ + @Override + public Object instantiateItem(ViewGroup container, int position) { + LayoutInflater inflater = LayoutInflater.from(mViewPager.getContext()); + View view = inflater.inflate(R.layout.album_art_view_pager_item, container, false); + if (mPlaybackManager != null) { + Image image = null; + boolean isArtistImage = false; + if (mSize > 0) { + if (mRepeatMode != PlaybackManager.NOT_REPEATING) { + position = position % mSize; + } + PlaylistEntry entry = mPlaybackManager.getPlaybackListEntry(position); + if (entry != null) { + image = entry.getQuery().getImage(); + isArtistImage = entry.getQuery().hasArtistImage(); + } + } else if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + StationPlaylist station = (StationPlaylist) mPlaybackManager.getPlaylist(); + if (station.getArtists() != null && station.getArtists().size() > 0) { + image = station.getArtists().get(0).first.getImage(); + isArtistImage = true; + } + if (image == null && station.getTracks() != null + && station.getTracks().size() > 0) { + image = station.getTracks().get(0).first.getImage(); + } + } + ImageView imageView = (ImageView) view.findViewById(R.id.album_art_image); + boolean landscapeMode = mViewPager.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + ImageUtils.loadImageIntoImageView(mViewPager.getContext(), imageView, + image, Image.getLargeImageSize(), landscapeMode, isArtistImage); + } + if (view != null) { + container.addView(view); + } + return view; + } + + /** + * @return If current {@link Playlist} is empty or null, return 1. If current {@link Playlist} + * is repeating, return FAKE_INFINITY_COUNT. Else return the current {@link Playlist}'s length. + */ + @Override + public int getCount() { + if (mPlaybackManager == null || mSize == 0) { + return 1; + } + if (mRepeatMode != PlaybackManager.NOT_REPEATING) { + return FAKE_INFINITY_COUNT; + } + return mSize; + } + + /** + * Remove the given {@link View} from the {@link ViewPager} + */ + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + + /** + * @return true if view is equal to object + */ + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + /** + * Dummy method + * + * @return always null + */ + @Override + public Parcelable saveState() { + return null; + } + + @Override + public int getItemPosition(Object object) { + return POSITION_NONE; + } + + /** + * @param entry to set the current item to + * @param smoothScroll boolean to determine whether or not to show a scrolling animation + */ + private void setCurrentItem(PlaylistEntry entry, boolean smoothScroll) { + int position = mPlaybackManager.getPlaybackListIndex(entry); + if (mRepeatMode != PlaybackManager.NOT_REPEATING) { + position += mFakeInfinityOffset; + } + if (position != mViewPager.getCurrentItem()) { + if (mRepeatMode != PlaybackManager.NOT_REPEATING) { + int currentItem = mViewPager.getCurrentItem(); + if (position == (currentItem % mSize) + 1 + || ((currentItem % mSize) == mSize - 1 && position == 0)) { + setCurrentViewPagerItem(mViewPager.getCurrentItem() + 1, smoothScroll); + } else if (position == (currentItem % mSize) - 1 + || ((currentItem % mSize) == 0 && position == mSize - 1)) { + setCurrentViewPagerItem(mViewPager.getCurrentItem() - 1, smoothScroll); + } else { + setCurrentViewPagerItem(position, false); + } + } else { + setCurrentViewPagerItem(position, smoothScroll); + } + } + mLastItem = position; + } + + private void setCurrentViewPagerItem(int position, boolean smoothScroll) { + mViewPager.clearOnPageChangeListeners(); + mViewPager.setCurrentItem(position, smoothScroll); + mViewPager.addOnPageChangeListener(mOnPageChangeListener); + } + + /** + * Update the {@link Playlist} of the {@link AlbumArtSwipeAdapter} with the current one in + * {@link org.tomahawk.tomahawk_android.services.PlaybackService} + */ + public void updatePlaylist() { + if (mPageScrollState != ViewPager.SCROLL_STATE_IDLE) { + mUpdateWhenIdle = true; + return; + } + if (mMediaController != null && mPlaybackManager != null) { + if (mMediaController.getPlaybackState().getExtras() != null) { + mRepeatMode = mMediaController.getPlaybackState().getExtras() + .getInt(PlaybackService.EXTRAS_KEY_REPEAT_MODE); + } else { + mRepeatMode = PlaybackManager.NOT_REPEATING; + } + mSize = mPlaybackManager.getPlaybackListSize(); + notifyDataSetChanged(); + if (mSize > 0) { + mFakeInfinityOffset = mSize * ((FAKE_INFINITY_COUNT / 2) / mSize); + setCurrentItem(mPlaybackManager.getCurrentEntry(), false); + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ClickListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ClickListener.java new file mode 100644 index 000000000..d895f9413 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ClickListener.java @@ -0,0 +1,55 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; + +import android.view.View; + +public class ClickListener implements View.OnClickListener, View.OnLongClickListener { + + private final Object mItem; + + private final Segment mSegment; + + private final MultiColumnClickListener mListener; + + public ClickListener(Object item, Segment segment, MultiColumnClickListener listener) { + mItem = item; + mListener = listener; + mSegment = segment; + } + + @Override + public void onClick(View view) { + mListener.onItemClick(view, mItem, mSegment); + } + + @Override + public boolean onLongClick(View view) { + return mListener.onItemLongClick(view, mItem, mSegment); + } + + public Object getItem() { + return mItem; + } + + public MultiColumnClickListener getListener() { + return mListener; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/CountryCodeSpinnerAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/CountryCodeSpinnerAdapter.java new file mode 100644 index 000000000..b52db7e5e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/CountryCodeSpinnerAdapter.java @@ -0,0 +1,68 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import android.content.Context; +import android.support.annotation.LayoutRes; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +public class CountryCodeSpinnerAdapter extends ArrayAdapter { + + private List mCodeNames; + + /** + * The resource indicating what views to inflate to display the content of this array adapter in + * a drop down widget. + */ + private int mDropDownResource; + + public CountryCodeSpinnerAdapter(Context context, int resource, + List codes, List codeNames) { + super(context, resource, codes); + + mCodeNames = codeNames; + } + + @Override + public void setDropDownViewResource(@LayoutRes int resource) { + this.mDropDownResource = resource; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + View view; + if (convertView == null) { + view = LayoutInflater.from(getContext()).inflate(mDropDownResource, parent, false); + } else { + view = convertView; + } + + // If no custom field is assigned, assume the whole resource is a TextView + TextView textView = (TextView) view; + String item = mCodeNames.get(position); + textView.setText(item); + + return view; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/DirectoryChooserAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/DirectoryChooserAdapter.java new file mode 100644 index 000000000..bb89108f8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/DirectoryChooserAdapter.java @@ -0,0 +1,241 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.views.DirectoryChooser; + +import android.graphics.Typeface; +import android.os.Build; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; + +import java.io.File; +import java.util.List; + +/** + * This class populates the listview inside the navigation drawer + */ +public class DirectoryChooserAdapter extends StickyBaseAdapter { + + private final LayoutInflater mLayoutInflater; + + private boolean mIsFirstRoot; + + private List mFolders; + + private final DirectoryChooser.DirectoryChooserListener mDirectoryChooserListener; + + public static class CustomDirectory { + + public File file; + + public boolean isWhitelisted; + + public boolean isMediaDirComplete; + } + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.adapters.DirectoryChooserAdapter} + * + * @param isFirstRoot true if the currentFolderRoot is the upmost root that should be reachable + */ + public DirectoryChooserAdapter(LayoutInflater layoutInflater, boolean isFirstRoot, + List folders, + DirectoryChooser.DirectoryChooserListener directoryChooserListener) { + mLayoutInflater = layoutInflater; + mIsFirstRoot = isFirstRoot; + mFolders = folders; + mDirectoryChooserListener = directoryChooserListener; + } + + public void update(boolean isFirstRoot, List folders) { + mIsFirstRoot = isFirstRoot; + mFolders = folders; + notifyDataSetChanged(); + } + + /** + * @return the count of every item to display + */ + @Override + public int getCount() { + return mFolders.size(); + } + + /** + * @return item for the given position + */ + @Override + public Object getItem(int position) { + return mFolders.get(position); + } + + /** + * Get the id of the item for the given position. (Id is equal to given position) + */ + @Override + public long getItemId(int position) { + return position; + } + + /** + * Get the correct {@link android.view.View} for the given position. + * + * @param position The position for which to get the correct {@link android.view.View} + * @param convertView The old {@link android.view.View}, which we might be able to recycle + * @param parent parental {@link android.view.ViewGroup} + * @return the correct {@link android.view.View} for the given position. + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + ViewHolder viewHolder; + if (convertView != null) { + viewHolder = (ViewHolder) convertView.getTag(); + view = convertView; + } else { + view = mLayoutInflater.inflate(R.layout.list_item_folder, parent, false); + viewHolder = new ViewHolder(view, R.layout.list_item_folder); + view.setTag(viewHolder); + } + + final CustomDirectory dir = (CustomDirectory) getItem(position); + + // Init checkbox + CheckBox checkBox = (CheckBox) viewHolder.findViewById(R.id.checkbox1); + CheckBox checkBox2 = (CheckBox) viewHolder.findViewById(R.id.checkbox2); + if (!dir.isMediaDirComplete) { + checkBox2.setButtonDrawable(R.drawable.abc_btn_check_to_on_mtrl_015_disabled); + checkBox.setVisibility(View.GONE); + checkBox2.setVisibility(View.VISIBLE); + } else { + checkBox.setVisibility(View.VISIBLE); + checkBox2.setVisibility(View.GONE); + } + checkBox.setOnCheckedChangeListener(null); + checkBox2.setOnCheckedChangeListener(null); + checkBox.setChecked(dir.isWhitelisted || !dir.isMediaDirComplete); + checkBox2.setChecked(dir.isWhitelisted || !dir.isMediaDirComplete); + CompoundButton.OnCheckedChangeListener listener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mDirectoryChooserListener.onDirectoryChecked(dir.file, isChecked); + } + }; + checkBox.setOnCheckedChangeListener(listener); + checkBox2.setOnCheckedChangeListener(listener); + + // Init textviews and main click listener + TextView textView = (TextView) viewHolder.findViewById(R.id.textview1); + textView.setText(getVisibleName(dir.file)); + textView.setTypeface(null, Typeface.NORMAL); + view.findViewById(R.id.browsable_indicator).setVisibility(View.INVISIBLE); + view.setOnClickListener(null); + if (dir.file.listFiles() != null + && dir.file.listFiles().length > 0) { + for (File file : dir.file.listFiles()) { + if (file.isDirectory()) { + textView.setTypeface(null, Typeface.BOLD); + ImageView browsableIndicator = + (ImageView) view.findViewById(R.id.browsable_indicator); + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), + browsableIndicator, R.drawable.ic_navigation_chevron_right, + android.R.color.black); + view.findViewById(R.id.browsable_indicator).setVisibility(View.VISIBLE); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mDirectoryChooserListener.onDirectoryBrowsed(dir.file); + } + }); + break; + } + } + } + return view; + } + + /** + * This method is being called by the StickyListHeaders library. Get the correct header {@link + * android.view.View} for the given position. + * + * @param position The position for which to get the correct {@link android.view.View} + * @param convertView The old {@link android.view.View}, which we might be able to recycle + * @param parent parental {@link android.view.ViewGroup} + * @return the correct header {@link android.view.View} for the given position. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + if (!mIsFirstRoot) { + View view; + ViewHolder viewHolder; + if (convertView != null) { + viewHolder = (ViewHolder) convertView.getTag(); + view = convertView; + } else { + LayoutInflater layoutInflater = LayoutInflater.from(TomahawkApp.getContext()); + view = layoutInflater.inflate(R.layout.list_item_folder_header, parent, false); + viewHolder = new ViewHolder(view, R.layout.list_item_folder_header); + view.setTag(viewHolder); + } + + TextView textView = (TextView) viewHolder.findViewById(R.id.textview1); + final CustomDirectory dir = (CustomDirectory) getItem(position); + if (dir != null) { + textView.setText("../" + getVisibleName(dir.file.getParentFile())); + } + + return view; + } else { + return new View(TomahawkApp.getContext()); + } + } + + /** + * This method is being called by the StickyListHeaders library. Returns the same value for each + * item that should be grouped under the same header. + * + * @param position the position of the item for which to get the header id + * @return the same value for each item that should be grouped under the same header. + */ + @Override + public long getHeaderId(int position) { + return 0; + } + + private String getVisibleName(File file) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Show "sdcard" for the user's folder when running in multi-user + if (file.getAbsolutePath() + .equals(Environment.getExternalStorageDirectory().getPath())) { + return TomahawkApp.getContext().getString(R.string.internal_storage); + } + } + return file.getName(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/FakePreferencesAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/FakePreferencesAdapter.java new file mode 100644 index 000000000..178bfafd4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/FakePreferencesAdapter.java @@ -0,0 +1,222 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.FakePreferenceGroup; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Since {@link android.preference.PreferenceFragment} is not supported with the official support + * library, and also not within ActionBarSherlock, we have to create our own Fragment with our own + * {@link FakePreferencesAdapter} + */ +public class FakePreferencesAdapter extends StickyBaseAdapter { + + private final LayoutInflater mLayoutInflater; + + private final List mFakePreferenceGroups; + + private class SpinnerListener implements AdapterView.OnItemSelectedListener { + + private final String mKey; + + public SpinnerListener(String key) { + mKey = key; + } + + @Override + public void onItemSelected(AdapterView parent, View view, + int position, long id) { + if (PreferenceUtils.getInt(mKey) != position) { + SharedPreferences.Editor editor = PreferenceUtils.edit(); + editor.putInt(mKey, position); + editor.commit(); + //TODO actually set bitrate + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + } + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.adapters.FakePreferencesAdapter} + */ + public FakePreferencesAdapter(LayoutInflater layoutInflater, + List fakePreferenceGroups) { + mLayoutInflater = layoutInflater; + mFakePreferenceGroups = fakePreferenceGroups; + } + + /** + * @return the total amount of all {@link FakePreferenceGroup}s this {@link + * FakePreferencesAdapter} displays + */ + @Override + public int getCount() { + int countSum = 0; + for (FakePreferenceGroup fakePreferenceGroup : mFakePreferenceGroups) { + countSum += fakePreferenceGroup.getFakePreferences().size(); + } + return countSum; + } + + /** + * Get the correct {@link org.tomahawk.tomahawk_android.utils.FakePreferenceGroup.FakePreference} + * for the given position + */ + @Override + public Object getItem(int position) { + Object item = null; + int offsetCounter = 0; + for (FakePreferenceGroup fakePreferenceGroup : mFakePreferenceGroups) { + if (position - offsetCounter < fakePreferenceGroup.getFakePreferences().size()) { + item = fakePreferenceGroup.getFakePreferences().get(position - offsetCounter); + break; + } + offsetCounter += fakePreferenceGroup.getFakePreferences().size(); + } + return item; + } + + /** + * Get the id of the item with the given position (the returned id is equal to the position) + */ + @Override + public long getItemId(int position) { + return position; + } + + /** + * Get the correct {@link View} for the given position. Recycle a convertView, if possible. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct {@link View} for the given position. + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = null; + FakePreferenceGroup.FakePreference item = + (FakePreferenceGroup.FakePreference) getItem(position); + + if (item != null) { + ViewHolder viewHolder = null; + if (convertView != null) { + viewHolder = (ViewHolder) convertView.getTag(); + view = convertView; + } + int viewType = getViewType(item); + if (viewHolder == null || viewHolder.mLayoutId != viewType) { + // If the viewHolder is null or the old viewType is different than the new one, + // we need to inflate a new view and construct a new viewHolder, + // which we set as the view's tag + view = mLayoutInflater.inflate(viewType, parent, false); + viewHolder = new ViewHolder(view, viewType); + view.setTag(viewHolder); + } else { + ImageView imageView = (ImageView) viewHolder.findViewById(R.id.imageview1); + if (imageView != null) { + imageView.setVisibility(View.GONE); + } + } + + // After we've set up the correct view and viewHolder, we now can fill the View's + // components with the correct data + if (viewHolder.mLayoutId == R.layout.fake_preferences_checkbox) { + boolean preferenceState = PreferenceUtils.getBoolean(item.storageKey); + CheckBox checkBox = (CheckBox) viewHolder.findViewById(R.id.checkbox1); + checkBox.setChecked(preferenceState); + } else if (viewHolder.mLayoutId == R.layout.fake_preferences_spinner) { + ArrayList list = new ArrayList<>(); + for (String headerString : TomahawkApp.getContext().getResources() + .getStringArray(R.array.fake_preferences_items_bitrate)) { + list.add(headerString.toUpperCase()); + } + ArrayAdapter adapter = new ArrayAdapter<>( + TomahawkApp.getContext(), R.layout.spinner_textview, list); + adapter.setDropDownViewResource(R.layout.spinner_dropdown_textview); + Spinner spinner = (Spinner) viewHolder.findViewById(R.id.spinner1); + spinner.setAdapter(adapter); + spinner.setSelection(PreferenceUtils.getInt(item.storageKey)); + spinner.setOnItemSelectedListener(new SpinnerListener(item.storageKey)); + } + TextView textView1 = (TextView) viewHolder.findViewById(R.id.textview1); + textView1.setText(item.title); + TextView textView2 = (TextView) viewHolder.findViewById(R.id.textview2); + textView2.setText(item.summary); + } + + // Finally we can return the correct view + return view; + } + + /** + * This method is being called by the StickyListHeaders library. Get the correct header {@link + * View} for the given position. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct header {@link View} for the given position. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + return new View(TomahawkApp.getContext()); + } + + /** + * This method is being called by the StickyListHeaders library. Returns the same value for each + * item that should be grouped under the same header. + * + * @param position the position of the item for which to get the header id + * @return the same value for each item that should be grouped under the same header. + */ + @Override + public long getHeaderId(int position) { + return 0; + } + + private int getViewType(FakePreferenceGroup.FakePreference item) { + if (item.type == FakePreferenceGroup.TYPE_CHECKBOX) { + return R.layout.fake_preferences_checkbox; + } else if (item.type == FakePreferenceGroup.TYPE_SPINNER) { + return R.layout.fake_preferences_spinner; + } + return R.layout.fake_preferences_plain; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/Segment.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/Segment.java new file mode 100644 index 000000000..fdbfa1adc --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/Segment.java @@ -0,0 +1,298 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.libtomahawk.collection.CollectionCursor; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; + +import android.content.res.Resources; +import android.widget.AdapterView; + +import java.util.ArrayList; +import java.util.List; + +public class Segment { + + private int mColumnCount = 1; + + private int mHorizontalPadding; + + private int mVerticalPadding; + + private int mHeaderLayoutId; + + private AdapterView.OnItemSelectedListener mSpinnerClickListener; + + private int mInitialPos; + + private final List mHeaderStrings = new ArrayList<>(); + + private List mListItems = new ArrayList<>(); + + private CollectionCursor mCollectionCursor; + + private Playlist mPlaylist; + + private PlaybackManager mPlaybackManager; + + private int mNumerationCorrection; + + private boolean mShowDuration; + + private boolean mShowNumeration; + + private boolean mHideArtistName; + + private boolean mShowResolverIcon; + + private int mLeftExtraPadding; + + public static class Builder { + + private Segment mSegment; + + public Builder(CollectionCursor collectionCursor) { + mSegment = new Segment(); + mSegment.mCollectionCursor = collectionCursor; + } + + public Builder(List listItems) { + mSegment = new Segment(); + mSegment.mListItems = listItems; + } + + public Builder(Playlist playlist) { + mSegment = new Segment(); + mSegment.mPlaylist = playlist; + } + + public Builder(PlaybackManager playbackManager) { + mSegment = new Segment(); + mSegment.mPlaybackManager = playbackManager; + } + + public Builder headerLayout(int headerLayoutId) { + mSegment.mHeaderLayoutId = headerLayoutId; + return this; + } + + public Builder headerString(int headerStringResId) { + mSegment.mHeaderStrings.add(TomahawkApp.getContext().getString(headerStringResId)); + return this; + } + + public Builder headerString(String headerString) { + mSegment.mHeaderStrings.add(headerString); + return this; + } + + public Builder headerStrings(List headerStringResIds) { + for (Integer resId : headerStringResIds) { + mSegment.mHeaderStrings.add(TomahawkApp.getContext().getString(resId)); + } + return this; + } + + public Builder spinner(AdapterView.OnItemSelectedListener spinnerClickListener, + int initialPos) { + mSegment.mSpinnerClickListener = spinnerClickListener; + mSegment.mInitialPos = initialPos; + return this; + } + + public Builder showAsGrid(int columnCountResId, int horizontalPaddingResId, + int verticalPaddingResId) { + Resources resources = TomahawkApp.getContext().getResources(); + mSegment.mColumnCount = resources.getInteger(columnCountResId); + mSegment.mHorizontalPadding = resources.getDimensionPixelSize(horizontalPaddingResId); + mSegment.mVerticalPadding = resources.getDimensionPixelSize(verticalPaddingResId); + return this; + } + + public Builder showDuration(boolean showDuration) { + mSegment.mShowDuration = showDuration; + return this; + } + + public Builder showNumeration(boolean showNumeration, int numerationCorrection) { + mSegment.mShowNumeration = showNumeration; + mSegment.mNumerationCorrection = numerationCorrection; + return this; + } + + public Builder hideArtistName(boolean hideArtistName) { + mSegment.mHideArtistName = hideArtistName; + return this; + } + + public Builder leftExtraPadding(int leftExtraPadding) { + mSegment.mLeftExtraPadding = leftExtraPadding; + return this; + } + + public Builder showResolverIcon(boolean showResolverIcon) { + mSegment.mShowResolverIcon = showResolverIcon; + return this; + } + + public Segment build() { + return mSegment; + } + + } + + private Segment() { + } + + public int getInitialPos() { + return mInitialPos; + } + + public String getHeaderString() { + if (mHeaderStrings.isEmpty()) { + return null; + } + return mHeaderStrings.get(0); + } + + public List getHeaderStrings() { + return mHeaderStrings; + } + + public int getHeaderLayoutId() { + return mHeaderLayoutId; + } + + public AdapterView.OnItemSelectedListener getSpinnerClickListener() { + return mSpinnerClickListener; + } + + public int getCount() { + if (mCollectionCursor != null) { + return mCollectionCursor.size(); + } else if (mPlaylist != null) { + return mPlaylist.size(); + } else if (mPlaybackManager != null) { + return mPlaybackManager.getPlaybackListSize() + - Math.max(0, mPlaybackManager.getCurrentIndex()); + } else { + return mListItems.size(); + } + } + + public int getRowCount() { + return (int) Math.ceil((float) getCount() / mColumnCount); + } + + public Object get(int location) { + if (mPlaybackManager != null) { + location = location + Math.max(0, mPlaybackManager.getCurrentIndex()); + } + if (mColumnCount > 1) { + List list = new ArrayList<>(); + for (int i = location * mColumnCount; i < location * mColumnCount + mColumnCount; i++) { + Object item = null; + if (mCollectionCursor != null && i < mCollectionCursor.size()) { + item = mCollectionCursor.get(i); + } else if (mPlaylist != null) { + item = mPlaylist.getEntryAtPos(i); + } else if (mPlaybackManager != null) { + item = mPlaybackManager.getPlaybackListEntry(i); + } else if (i < mListItems.size()) { + item = mListItems.get(i); + } + list.add(item); + } + return list; + } else { + Object item; + if (mCollectionCursor != null) { + item = mCollectionCursor.get(location); + } else if (mPlaylist != null) { + item = mPlaylist.getEntryAtPos(location); + } else if (mPlaybackManager != null) { + item = mPlaybackManager.getPlaybackListEntry(location); + } else { + item = mListItems.get(location); + } + return item; + } + } + + public Object getFirstSegmentItem() { + Object result = get(0); + if (result instanceof List) { + return ((List) get(0)).get(0); + } else { + return result; + } + } + + public void close() { + if (mCollectionCursor != null) { + mCollectionCursor.close(); + } + } + + public int getHorizontalPadding() { + return mHorizontalPadding; + } + + public int getVerticalPadding() { + return mVerticalPadding; + } + + public boolean isShowAsQueued(int position) { + if (mPlaybackManager != null) { + position = position + Math.max(0, mPlaybackManager.getCurrentIndex()); + return mPlaybackManager.isPartOfQueue(position); + } + return false; + } + + public boolean isShowDuration() { + return mShowDuration; + } + + public boolean isShowNumeration() { + return mShowNumeration; + } + + public boolean isShowResolverIcon() { + return mShowResolverIcon; + } + + public int getNumeration(int position) { + if (mPlaybackManager == null) { + return position + mNumerationCorrection; + } else { + position = position + Math.max(0, mPlaybackManager.getCurrentIndex()); + return mPlaybackManager.getNumeration(position); + } + } + + public boolean isHideArtistName() { + return mHideArtistName; + } + + public int getLeftExtraPadding() { + return mLeftExtraPadding; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/StickyBaseAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/StickyBaseAdapter.java new file mode 100644 index 000000000..e7369c648 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/StickyBaseAdapter.java @@ -0,0 +1,30 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import android.widget.BaseAdapter; + +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; + +/** + * This class is used to populate a {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView}. + */ +public abstract class StickyBaseAdapter extends BaseAdapter + implements StickyListHeadersAdapter { + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/SuggestionSimpleCursorAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/SuggestionSimpleCursorAdapter.java new file mode 100644 index 000000000..487354dc0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/SuggestionSimpleCursorAdapter.java @@ -0,0 +1,40 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.libtomahawk.database.TomahawkSQLiteHelper; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.SimpleCursorAdapter; + +public class SuggestionSimpleCursorAdapter extends SimpleCursorAdapter { + + public SuggestionSimpleCursorAdapter(Context context, int layout, Cursor c, + String[] from, int[] to, int flags) { + super(context, layout, c, from, to, flags); + } + + @Override + public CharSequence convertToString(Cursor cursor) { + int indexColumnSuggestion = cursor + .getColumnIndex(TomahawkSQLiteHelper.SEARCHHISTORY_COLUMN_ENTRY); + + return cursor.getString(indexColumnSuggestion); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkListAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkListAdapter.java new file mode 100644 index 000000000..c26400ba9 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkListAdapter.java @@ -0,0 +1,813 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import com.daimajia.swipe.SwipeLayout; +import com.daimajia.swipe.implments.SwipeItemAdapterMangerImpl; +import com.daimajia.swipe.interfaces.SwipeAdapterInterface; +import com.daimajia.swipe.interfaces.SwipeItemMangerInterface; +import com.daimajia.swipe.util.Attributes; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.ListItemDrawable; +import org.tomahawk.libtomahawk.collection.ListItemString; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.BiDirectionalFrame; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import java.util.ArrayList; +import java.util.List; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +/** + * This class is used to populate a {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView}. + */ +public class TomahawkListAdapter extends StickyBaseAdapter implements + SwipeItemMangerInterface, SwipeAdapterInterface { + + private static final String TAG = TomahawkListAdapter.class.getSimpleName(); + + private final TomahawkMainActivity mActivity; + + private List mSegments; + + private int mRowCount; + + private Collection mCollection; + + private final MultiColumnClickListener mClickListener; + + private final LayoutInflater mLayoutInflater; + + private PlaylistEntry mHighlightedPlaylistEntry; + + private Query mHighlightedQuery; + + private boolean mHighlightedItemIsPlaying = false; + + private int mHeaderSpacerHeight = 0; + + private View mHeaderSpacerForwardView; + + private int mFooterSpacerHeight = 0; + + private ProgressBar mProgressBar; + + private final SwipeItemAdapterMangerImpl mItemManager = new SwipeItemAdapterMangerImpl(this); + + /** + * Constructs a new {@link TomahawkListAdapter}. + */ + public TomahawkListAdapter(TomahawkMainActivity activity, LayoutInflater layoutInflater, + List segments, Collection collection, StickyListHeadersListView listView, + MultiColumnClickListener clickListener) { + mActivity = activity; + mLayoutInflater = layoutInflater; + mClickListener = clickListener; + setSegments(segments); + updateFooterSpacerHeight(listView); + mItemManager.setMode(Attributes.Mode.Single); + mCollection = collection; + } + + /** + * Constructs a new {@link TomahawkListAdapter}. + */ + public TomahawkListAdapter(TomahawkMainActivity activity, LayoutInflater layoutInflater, + List segments, StickyListHeadersListView listView, + MultiColumnClickListener clickListener) { + mActivity = activity; + mLayoutInflater = layoutInflater; + mClickListener = clickListener; + setSegments(segments); + updateFooterSpacerHeight(listView); + mItemManager.setMode(Attributes.Mode.Single); + } + + /** + * Constructs a new {@link TomahawkListAdapter}. + */ + public TomahawkListAdapter(TomahawkMainActivity activity, LayoutInflater layoutInflater, + Segment segment, StickyListHeadersListView listView, + MultiColumnClickListener clickListener) { + mActivity = activity; + mLayoutInflater = layoutInflater; + mClickListener = clickListener; + List segments = new ArrayList<>(); + segments.add(segment); + setSegments(segments); + updateFooterSpacerHeight(listView); + mItemManager.setMode(Attributes.Mode.Single); + } + + /** + * Set the complete list of {@link Segment} + */ + public void setSegments(List segments, StickyListHeadersListView listView) { + setSegments(segments); + + updateFooterSpacerHeight(listView); + notifyDataSetChanged(); + } + + private void setSegments(List segments) { + closeSegments(segments); + mSegments = segments; + mRowCount = 0; + for (Segment segment : mSegments) { + mRowCount += segment.getRowCount(); + } + } + + public void closeSegments(List newSegments) { + if (mSegments != null) { + for (Segment segment : mSegments) { + if (newSegments == null || !newSegments.contains(segment)) { + segment.close(); + } + } + } + } + + public void setShowContentHeaderSpacer(int headerSpacerHeight, + StickyListHeadersListView listView, View headerSpacerForwardView) { + mHeaderSpacerHeight = headerSpacerHeight; + mHeaderSpacerForwardView = headerSpacerForwardView; + updateFooterSpacerHeight(listView); + } + + public void setHighlightedItemIsPlaying(boolean highlightedItemIsPlaying) { + mHighlightedItemIsPlaying = highlightedItemIsPlaying; + } + + /** + * set the position of the item, which should be highlighted + */ + public void setHighlightedQuery(Query query) { + mHighlightedQuery = query; + } + + /** + * set the position of the item, which should be highlighted + */ + public void setHighlightedEntry(PlaylistEntry entry) { + mHighlightedPlaylistEntry = entry; + } + + /** + * Get the correct {@link View} for the given position. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct {@link View} for the given position. + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = null; + Object o = getItem(position); + + // Don't display the socialAction item directly, but rather the item that is its target + if (o instanceof SocialAction && ((SocialAction) o).getTargetObject() != null) { + o = ((SocialAction) o).getTargetObject(); + } + + boolean shouldBeHighlighted = false; + if (o instanceof SocialAction) { + shouldBeHighlighted = mHighlightedQuery != null + && ((SocialAction) o).getQuery() == mHighlightedQuery; + } else if (o instanceof PlaylistEntry) { + shouldBeHighlighted = mHighlightedPlaylistEntry != null + && o == mHighlightedPlaylistEntry; + } else if (o instanceof Query) { + shouldBeHighlighted = mHighlightedQuery != null + && o == mHighlightedQuery; + } + + List viewHolders = new ArrayList<>(); + if (convertView != null) { + viewHolders = (List) convertView.getTag(); + view = convertView; + } + int viewType = getViewType(o, position, mHeaderSpacerHeight > 0 && position == 0, + position == getCount() - 1); + int expectedViewHoldersCount = 1; + if (o instanceof List) { + expectedViewHoldersCount = 0; + for (Object object : (List) o) { + if (object != null) { + expectedViewHoldersCount++; + } + } + } + if (viewHolders.size() != expectedViewHoldersCount + || viewHolders.size() == 0 || viewHolders.get(0).mLayoutId != viewType) { + // If the viewHolder is null or the old viewType is different than the new one, + // we need to inflate a new view and construct a new viewHolder, + // which we set as the view's tag + viewHolders = new ArrayList<>(); + if (o instanceof List) { + LinearLayout rowContainer = (LinearLayout) mLayoutInflater + .inflate(R.layout.row_container, parent, false); + rowContainer.setPadding(rowContainer.getPaddingLeft(), + getSegment(position).getVerticalPadding(), rowContainer.getPaddingRight(), + 0); + for (int i = 0; i < ((List) o).size(); i++) { + if (((List) o).get(i) != null) { + View gridItem = mLayoutInflater.inflate(viewType, rowContainer, false); + ViewHolder viewHolder = new ViewHolder(gridItem, viewType); + rowContainer.addView(gridItem); + viewHolders.add(viewHolder); + if (i < ((List) o).size() - 1) { + View spacer = new View(mLayoutInflater.getContext()); + spacer.setLayoutParams(new ViewGroup.LayoutParams( + getSegment(position).getHorizontalPadding(), + ViewGroup.LayoutParams.MATCH_PARENT)); + rowContainer.addView(spacer); + } + } else { + rowContainer.addView(mLayoutInflater + .inflate(R.layout.row_container_spacer, rowContainer, false)); + } + } + view = rowContainer; + } else { + view = mLayoutInflater.inflate(viewType, parent, false); + ViewHolder viewHolder = new ViewHolder(view, viewType); + viewHolders.add(viewHolder); + if (view instanceof SwipeLayout) { + mItemManager.initialize(view, position); + if (!PreferenceUtils.getBoolean( + PreferenceUtils.COACHMARK_SWIPELAYOUT_ENQUEUE_DISABLED)) { + ((SwipeLayout) view).addSwipeListener(new SwipeLayout.SwipeListener() { + @Override + public void onStartOpen(SwipeLayout swipeLayout) { + } + + @Override + public void onOpen(SwipeLayout swipeLayout) { + if (!PreferenceUtils.getBoolean( + PreferenceUtils.COACHMARK_SWIPELAYOUT_ENQUEUE_DISABLED)) { + final View coachMark = ViewUtils.ensureInflation( + swipeLayout, R.id.swipelayout_enqueue_coachmark_stub, + R.id.swipelayout_enqueue_coachmark); + coachMark.setVisibility(View.VISIBLE); + View closeButton = coachMark.findViewById(R.id.close_button); + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PreferenceUtils.edit().putBoolean( + PreferenceUtils.COACHMARK_SWIPELAYOUT_ENQUEUE_DISABLED, + true).apply(); + coachMark.setVisibility(View.GONE); + } + }); + } + } + + @Override + public void onStartClose(SwipeLayout swipeLayout) { + } + + @Override + public void onClose(SwipeLayout swipeLayout) { + View coachMark = swipeLayout.findViewById( + R.id.swipelayout_enqueue_coachmark); + if (coachMark != null) { + coachMark.setVisibility(View.GONE); + } + } + + @Override + public void onUpdate(SwipeLayout swipeLayout, int i, int i1) { + } + + @Override + public void onHandRelease(SwipeLayout swipeLayout, float v, float v1) { + } + }); + } + } + } + // Set extra padding + if (getSegment(position) != null && getSegment(position).getLeftExtraPadding() > 0) { + if (view instanceof SwipeLayout) { + // if it's a SwipeLayout, we have to set the padding on the foreground + // layout within the SwipeLayout instead + View foreground = ((ViewGroup) view).getChildAt(1); + foreground.setPadding(foreground.getPaddingLeft() + getSegment(position) + .getLeftExtraPadding(), + foreground.getPaddingTop(), foreground.getPaddingRight(), + foreground.getPaddingBottom()); + } else { + view.setPadding( + view.getPaddingLeft() + getSegment(position).getLeftExtraPadding(), + view.getPaddingTop(), + view.getPaddingRight(), view.getPaddingBottom()); + } + } + view.setTag(viewHolders); + } else { + if (view instanceof SwipeLayout) { + // We have a SwipeLayout + mItemManager.updateConvertView(view, position); + } + } + + // After we've setup the correct view and viewHolder, we now can fill the View's + // components with the correct data + for (int i = 0; i < viewHolders.size(); i++) { + ViewHolder viewHolder = viewHolders.get(i); + Object item = getItem(position); + if (item instanceof List) { + item = ((List) item).get(i); + } + if (item == null) { + if (viewHolder.mLayoutId == R.layout.content_footer_spacer) { + view.setLayoutParams( + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + mFooterSpacerHeight)); + } else if (viewHolder.mLayoutId == R.layout.content_header_spacer) { + view.setLayoutParams( + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + mHeaderSpacerHeight)); + if (mHeaderSpacerForwardView != null) { + BiDirectionalFrame biDirectionalFrame = (BiDirectionalFrame) view; + biDirectionalFrame.setForwardView(mHeaderSpacerForwardView); + } + } + } else { + Object targetItem = item; + // Don't display the socialAction item directly, but rather the item that is its target + if (item instanceof SocialAction + && ((SocialAction) item).getTargetObject() != null) { + targetItem = ((SocialAction) item).getTargetObject(); + } + if (viewHolder.mLayoutId == R.layout.grid_item_artist + || viewHolder.mLayoutId == R.layout.list_item_artist) { + String numerationString = null; + if (getSegment(position).isShowNumeration()) { + int pos = getPosInSegment(position) * viewHolders.size() + i; + numerationString = "" + getSegment(position).getNumeration(pos); + } + viewHolder.fillView((Artist) targetItem, numerationString); + } else if (viewHolder.mLayoutId == R.layout.grid_item_album + || viewHolder.mLayoutId == R.layout.list_item_album) { + String numerationString = null; + if (getSegment(position).isShowNumeration()) { + int pos = getPosInSegment(position) * viewHolders.size() + i; + numerationString = "" + getSegment(position).getNumeration(pos); + } + viewHolder.fillView((Album) targetItem, mCollection, numerationString); + } else if (viewHolder.mLayoutId == R.layout.grid_item_resolver) { + viewHolder.fillView((Resolver) targetItem); + } else if (viewHolder.mLayoutId == R.layout.grid_item_playlist + || viewHolder.mLayoutId == R.layout.grid_item_station) { + if (targetItem instanceof StationPlaylist) { + viewHolder.fillView((StationPlaylist) targetItem); + } else { + viewHolder.fillView((Playlist) targetItem); + } + } else if (viewHolder.mLayoutId == R.layout.grid_item_user + || viewHolder.mLayoutId == R.layout.list_item_user) { + viewHolder.fillView((User) targetItem); + } else if (viewHolder.mLayoutId == R.layout.single_line_list_item) { + viewHolder.fillView(((Playlist) targetItem).getName()); + } else if (viewHolder.mLayoutId == R.layout.list_item_text + || viewHolder.mLayoutId == R.layout.list_item_text_highlighted) { + viewHolder.fillView(((ListItemString) targetItem).getText()); + } else if (viewHolder.mLayoutId == R.layout.list_item_image) { + viewHolder.fillView((ListItemDrawable) targetItem); + } else if (viewHolder.mLayoutId == R.layout.list_item_track_artist + || viewType == R.layout.list_item_numeration_track_artist + || viewType == R.layout.list_item_numeration_track_duration + || viewType == R.layout.list_item_track_artist_queued) { + if (targetItem instanceof Track) { + viewHolder.fillView((Track) targetItem); + } else { + View coachMark = + viewHolder.findViewById(R.id.swipelayout_enqueue_coachmark); + if (coachMark != null) { + coachMark.setVisibility(View.GONE); + } + String numerationString = null; + boolean isShowAsQueued = + getSegment(position).isShowAsQueued(getPosInSegment(position)); + if (!isShowAsQueued && getSegment(position).isShowNumeration()) { + int pos = getPosInSegment(position) * viewHolders.size() + i; + numerationString = String.format("%02d", + getSegment(position).getNumeration(pos)); + } + final Query query; + final PlaylistEntry entry; + if (targetItem instanceof PlaylistEntry) { + query = ((PlaylistEntry) targetItem).getQuery(); + entry = (PlaylistEntry) targetItem; + } else { + query = (Query) targetItem; + entry = null; + } + + View.OnClickListener dequeueListener = null; + if (mHighlightedPlaylistEntry != entry) { + dequeueListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle extras = new Bundle(); + extras.putString(TomahawkFragment.PLAYLISTENTRY, + entry.getCacheKey()); + mActivity.getSupportMediaController().getTransportControls() + .sendCustomAction( + PlaybackService.ACTION_DELETE_ENTRY_IN_QUEUE, + extras); + } + }; + } + viewHolder.fillView(query, numerationString, + mHighlightedItemIsPlaying && shouldBeHighlighted, isShowAsQueued, + dequeueListener, getSegment(position).isShowResolverIcon()); + + FrameLayout progressBarContainer = (FrameLayout) viewHolder + .findViewById(R.id.progressbar_container); + if (mHighlightedItemIsPlaying && shouldBeHighlighted) { + if (mProgressBar == null) { + mProgressBar = (ProgressBar) mLayoutInflater + .inflate(R.layout.progressbar, + progressBarContainer, false); + } + if (mProgressBar.getParent() instanceof FrameLayout) { + ((FrameLayout) mProgressBar.getParent()).removeView(mProgressBar); + } + progressBarContainer.addView(mProgressBar); + } else { + progressBarContainer.removeAllViews(); + } + } + } + + //Set up the click listeners + viewHolder.setMainClickListener(item, getSegment(position), mClickListener); + } + } + return view; + } + + /** + * @return the count of every item to display + */ + @Override + public int getCount() { + return mRowCount + (mHeaderSpacerHeight > 0 ? 1 : 0) + 1; + } + + /** + * @return item for the given position + */ + @Override + public Object getItem(int position) { + if (mHeaderSpacerHeight > 0) { + if (position == 0) { + return null; + } else { + position--; + } + } + int counter = 0; + int correctedPos = position; + for (Segment segment : mSegments) { + counter += segment.getRowCount(); + if (position < counter) { + return segment.get(correctedPos); + } else { + correctedPos -= segment.getRowCount(); + } + } + return null; + } + + public Segment getSegment(int position) { + if (mHeaderSpacerHeight > 0) { + if (position == 0) { + return null; + } else { + position--; + } + } + int counter = 0; + for (Segment segment : mSegments) { + counter += segment.getRowCount(); + if (position < counter) { + return segment; + } + } + return null; + } + + public int getPosInSegment(int position) { + if (mHeaderSpacerHeight > 0) { + if (position == 0) { + return 0; + } else { + position--; + } + } + int counter = 0; + int correctedPos = position; + for (Segment segment : mSegments) { + counter += segment.getRowCount(); + if (position < counter) { + return correctedPos; + } else { + correctedPos -= segment.getRowCount(); + } + } + return 0; + } + + /** + * Get the id of the item for the given position. (Id is equal to given position) + */ + @Override + public long getItemId(int position) { + return position; + } + + /** + * This method is being called by the StickyListHeaders library. Get the correct header {@link + * View} for the given position. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct header {@link View} for the given position. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + Segment segment = getSegment(position); + if (segment != null && segment.getHeaderLayoutId() > 0) { + View view = null; + ViewHolder viewHolder = null; + if (convertView != null) { + viewHolder = (ViewHolder) convertView.getTag(); + view = convertView; + } + int layoutId = getHeaderViewType(segment); + if (viewHolder == null || viewHolder.mLayoutId != layoutId) { + view = mLayoutInflater.inflate(layoutId, null); + viewHolder = new ViewHolder(view, layoutId); + view.setTag(viewHolder); + } + + if (layoutId == R.layout.dropdown_header) { + ArrayList spinnerItems = new ArrayList<>(); + for (String headerString : segment.getHeaderStrings()) { + spinnerItems.add(headerString.toUpperCase()); + } + viewHolder.fillHeaderView(spinnerItems, segment.getInitialPos(), + segment.getSpinnerClickListener()); + } else if (layoutId == R.layout.single_line_list_header) { + viewHolder.fillHeaderView(segment.getHeaderString().toUpperCase()); + } else if (layoutId == R.layout.list_header_socialaction_fake) { + viewHolder.fillHeaderView(segment.getHeaderString()); + } else if (layoutId == R.layout.list_header_socialaction) { + SocialAction socialAction = (SocialAction) segment.getFirstSegmentItem(); + viewHolder.fillHeaderView(socialAction, segment.getCount()); + } + return view; + } else { + return new View(mActivity); + } + } + + /** + * This method is being called by the StickyListHeaders library. Returns the same value for each + * item that should be grouped under the same header. + * + * @param position the position of the item for which to get the header id + * @return the same value for each item that should be grouped under the same header. + */ + @Override + public long getHeaderId(int position) { + Segment segment = getSegment(position); + if (segment != null) { + return mSegments.indexOf(segment); + } else { + return -1; + } + } + + private int getViewType(Object item, int position, boolean isContentHeaderItem, + boolean isFooter) { + if (item instanceof List) { + // We have a grid item + // Don't display the socialAction item directly, but rather the item that is its target + if (!((List) item).isEmpty()) { + Object firstItem = ((List) item).get(0); + if (firstItem instanceof SocialAction + && ((SocialAction) firstItem).getTargetObject() != null) { + firstItem = ((SocialAction) firstItem).getTargetObject(); + } + if (firstItem instanceof User) { + return R.layout.grid_item_user; + } else if (firstItem instanceof Resolver) { + return R.layout.grid_item_resolver; + } else if (firstItem instanceof Artist) { + return R.layout.grid_item_artist; + } else if (firstItem instanceof Album) { + return R.layout.grid_item_album; + } else if (firstItem instanceof StationPlaylist) { + return R.layout.grid_item_station; + } else if (firstItem instanceof Playlist) { + return R.layout.grid_item_playlist; + } else { + Log.e(TAG, "getViewType - Couldn't find appropriate viewType!"); + return 0; + } + } + } + if (item instanceof SocialAction && ((SocialAction) item).getTargetObject() != null) { + item = ((SocialAction) item).getTargetObject(); + } + Segment segment = getSegment(position); + if (isContentHeaderItem) { + return R.layout.content_header_spacer; + } else if (isFooter) { + return R.layout.content_footer_spacer; + } else if (item instanceof Playlist) { + return R.layout.single_line_list_item; + } else if (item instanceof ListItemString) { + if (((ListItemString) item).isHighlighted()) { + return R.layout.list_item_text_highlighted; + } else { + return R.layout.list_item_text; + } + } else if (item instanceof ListItemDrawable) { + return R.layout.list_item_image; + } else if (item instanceof Album) { + return R.layout.list_item_album; + } else if (item instanceof Artist) { + return R.layout.list_item_artist; + } else if (item instanceof User) { + return R.layout.list_item_user; + } else if (segment != null && segment.isHideArtistName()) { + return R.layout.list_item_numeration_track_duration; + } else if (segment != null && segment.isShowAsQueued(getPosInSegment(position))) { + return R.layout.list_item_track_artist_queued; + } else if (segment != null && segment.isShowNumeration()) { + return R.layout.list_item_numeration_track_artist; + } else if (segment != null && segment.isShowResolverIcon()) { + return R.layout.list_item_numeration_track_artist; + } else { + return R.layout.list_item_track_artist; + } + } + + private int getHeaderViewType(Segment segment) { + return segment.getHeaderLayoutId(); + } + + private void updateFooterSpacerHeight(final StickyListHeadersListView listView) { + if (mHeaderSpacerHeight > 0) { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(listView) { + @Override + public void run() { + mFooterSpacerHeight = calculateFooterSpacerHeight(listView); + notifyDataSetChanged(); + } + }); + } + } + + private int calculateFooterSpacerHeight(StickyListHeadersListView listView) { + if (getCount() > 10) { + return 0; + } + int footerSpacerHeight = listView.getWrappedList().getHeight(); + long headerId = getHeaderId(0); + for (int i = 1; i < getCount(); i++) { + View view = getView(i, null, listView.getWrappedList()); + if (view != null) { + view.measure(View.MeasureSpec.makeMeasureSpec(0, + View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, + View.MeasureSpec.UNSPECIFIED)); + footerSpacerHeight -= view.getMeasuredHeight(); + } + if (headerId != getHeaderId(i)) { + headerId = getHeaderId(i); + View headerView = getHeaderView(i, null, listView.getWrappedList()); + if (headerView != null) { + headerView.measure(View.MeasureSpec.makeMeasureSpec(0, + View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, + View.MeasureSpec.UNSPECIFIED)); + footerSpacerHeight -= headerView.getMeasuredHeight(); + } + } + if (footerSpacerHeight <= 0) { + footerSpacerHeight = 0; + break; + } + } + return footerSpacerHeight; + } + + @Override + public int getSwipeLayoutResourceId(int position) { + return 0; + } + + @Override + public void openItem(int position) { + mItemManager.openItem(position); + } + + @Override + public void closeItem(int position) { + mItemManager.closeItem(position); + } + + @Override + public void closeAllExcept(SwipeLayout layout) { + mItemManager.closeAllExcept(layout); + } + + @Override + public void closeAllItems() { + mItemManager.closeAllItems(); + } + + @Override + public List getOpenItems() { + return mItemManager.getOpenItems(); + } + + @Override + public List getOpenLayouts() { + return mItemManager.getOpenLayouts(); + } + + @Override + public void removeShownLayouts(SwipeLayout layout) { + mItemManager.removeShownLayouts(layout); + } + + @Override + public boolean isOpen(int position) { + return mItemManager.isOpen(position); + } + + @Override + public Attributes.Mode getMode() { + return mItemManager.getMode(); + } + + @Override + public void setMode(Attributes.Mode mode) { + mItemManager.setMode(mode); + } + + public ProgressBar getProgressBar() { + return mProgressBar; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkMenuAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkMenuAdapter.java new file mode 100644 index 000000000..a1c12369a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkMenuAdapter.java @@ -0,0 +1,193 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import com.github.rahatarmanahmed.cpv.CircularProgressView; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverCollectionMetaData; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class populates the listview inside the navigation drawer + */ +public class TomahawkMenuAdapter extends StickyBaseAdapter { + + private List mResourceHolders = new ArrayList<>(); + + public static class ResourceHolder { + + public String id; + + public String title; + + public int iconResId; + + public ScriptResolverCollection collection; + + public Image image; + + public User user; + + public boolean isLoading; + } + + /** + * Constructs a new {@link TomahawkMenuAdapter} + */ + public TomahawkMenuAdapter(ArrayList resourceHolders) { + mResourceHolders = resourceHolders; + } + + public void setResourceHolders(ArrayList resourceHolders) { + mResourceHolders = resourceHolders; + notifyDataSetChanged(); + } + + /** + * @return the count of every item to display + */ + @Override + public int getCount() { + return mResourceHolders.size(); + } + + /** + * @return item for the given position + */ + @Override + public Object getItem(int position) { + return mResourceHolders.get(position); + } + + /** + * Get the id of the item for the given position. (Id is equal to given position) + */ + @Override + public long getItemId(int position) { + return position; + } + + /** + * Get the correct {@link View} for the given position. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct {@link View} for the given position. + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Object item = getItem(position); + ResourceHolder holder = (ResourceHolder) item; + LayoutInflater inflater = LayoutInflater.from(TomahawkApp.getContext()); + if (((ResourceHolder) item).user != null) { + View contentHeaderView = + inflater.inflate(R.layout.content_header_user_navdrawer, parent, false); + TextView textView = (TextView) contentHeaderView.findViewById(R.id.textview1); + textView.setText(holder.title.toUpperCase()); + TextView userTextView = (TextView) contentHeaderView.findViewById(R.id.usertextview1); + ImageView userImageView = + (ImageView) contentHeaderView.findViewById(R.id.userimageview1); + ImageUtils.loadUserImageIntoImageView(TomahawkApp.getContext(), userImageView, + holder.user, Image.getSmallImageSize(), userTextView); + userImageView.setVisibility(View.VISIBLE); + return contentHeaderView; + } else { + View view = inflater.inflate(R.layout.single_line_list_menu, parent, false); + final TextView textView = (TextView) view + .findViewById(R.id.single_line_list_menu_textview); + final ImageView imageView = (ImageView) view.findViewById(R.id.icon_menu_imageview); + if (holder.collection != null) { + holder.collection.getMetaData().done( + new DoneCallback() { + @Override + public void onDone(ScriptResolverCollectionMetaData result) { + textView.setText(result.prettyname); + } + }); + holder.collection.loadIcon(imageView, false); + } else { + textView.setText(holder.title.toUpperCase()); + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + holder.iconResId); + } + CircularProgressView progressView = + (CircularProgressView) view.findViewById(R.id.circularprogressview); + if (holder.isLoading) { + progressView.startAnimation(); + progressView.setVisibility(View.VISIBLE); + } else { + progressView.setVisibility(View.GONE); + } + return view; + } + } + + /** + * This method is being called by the StickyListHeaders library. Get the correct header {@link + * View} for the given position. + * + * @param position The position for which to get the correct {@link View} + * @param convertView The old {@link View}, which we might be able to recycle + * @param parent parental {@link ViewGroup} + * @return the correct header {@link View} for the given position. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + if (mResourceHolders.get(position).collection != null) { + View headerView = LayoutInflater.from(TomahawkApp.getContext()) + .inflate(R.layout.menu_header_cloudcollection, parent, false); + TextView textView = (TextView) headerView.findViewById(R.id.textview1); + textView.setText(TomahawkApp.getContext().getString( + R.string.drawer_header_cloudcollections).toUpperCase()); + return headerView; + } else { + return new View(TomahawkApp.getContext()); + } + } + + /** + * This method is being called by the StickyListHeaders library. Returns the same value for each + * item that should be grouped under the same header. + * + * @param position the position of the item for which to get the header id + * @return the same value for each item that should be grouped under the same header. + */ + @Override + public long getHeaderId(int position) { + if (mResourceHolders.get(position).collection != null) { + return 1; + } + return 0; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkPagerAdapter.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkPagerAdapter.java new file mode 100755 index 000000000..b9a3df45b --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/TomahawkPagerAdapter.java @@ -0,0 +1,84 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +import java.util.List; + +public class TomahawkPagerAdapter extends FragmentStatePagerAdapter { + + private final Class mContainerFragmentClass; + + private final long mContainerFragmentId; + + private List mFragmentInfos; + + public TomahawkPagerAdapter(FragmentManager fragmentManager, List fragmentInfos, + Class containerFragmentClass, long containerFragmentId) { + super(fragmentManager); + + mFragmentInfos = fragmentInfos; + mContainerFragmentClass = containerFragmentClass; + mContainerFragmentId = containerFragmentId; + } + + @Override + public Fragment getItem(int position) { + Bundle bundle = mFragmentInfos.get(position).mBundle; + bundle.putString(TomahawkFragment.CONTAINER_FRAGMENT_CLASSNAME, + mContainerFragmentClass.getName()); + bundle.putInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE, position); + bundle.putLong(TomahawkFragment.CONTAINER_FRAGMENT_ID, mContainerFragmentId); + return Fragment.instantiate(TomahawkApp.getContext(), + mFragmentInfos.get(position).mClass.getName(), bundle); + } + + @Override + public int getCount() { + return mFragmentInfos.size(); + } + + @Override + public int getItemPosition(Object object) { + return POSITION_NONE; + } + + @Override + public CharSequence getPageTitle(int position) { + return mFragmentInfos.get(position).mTitle; + } + + public void changeFragment(int position, FragmentInfo fragmentInfo) { + mFragmentInfos.remove(position); + mFragmentInfos.add(position, fragmentInfo); + notifyDataSetChanged(); + } + + public void changeFragments(List fragmentInfos) { + mFragmentInfos = fragmentInfos; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ViewHolder.java b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ViewHolder.java new file mode 100644 index 000000000..69eb1c197 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/adapters/ViewHolder.java @@ -0,0 +1,535 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.adapters; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.ListItemDrawable; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.infosystem.hatchet.HatchetInfoPlugin; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; +import org.tomahawk.tomahawk_android.views.PlaybackPanel; + +import android.content.res.Resources; +import android.support.v4.util.Pair; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ViewHolder { + + final int mLayoutId; + + private final View mRootView; + + private final Map mCachedViews = new HashMap<>(); + + private ClickListener mMainClickListener; + + public ViewHolder(View rootView, int layoutId) { + mLayoutId = layoutId; + mRootView = rootView; + } + + public View ensureInflation(int stubResId, int inflatedId) { + return ViewUtils.ensureInflation(mRootView, stubResId, inflatedId); + } + + public View findViewById(int id) { + if (mCachedViews.containsKey(id)) { + return mCachedViews.get(id); + } else { + View view = mRootView.findViewById(id); + if (view != null) { + mCachedViews.put(id, view); + } + return view; + } + } + + public void setMainClickListener(Object item, Segment segment, MultiColumnClickListener listener) { + if (mMainClickListener == null || item != mMainClickListener.getItem() + || listener != mMainClickListener.getListener()) { + View view = findViewById(R.id.mainclickarea); + if (view == null) { + view = mRootView; + } + ClickListener clickListener = new ClickListener(item, segment, listener); + view.setOnClickListener(clickListener); + view.setOnLongClickListener(clickListener); + mMainClickListener = clickListener; + } + } + + public void fillView(Query query, String numerationString, boolean showAsPlaying, + boolean showAsQueued, View.OnClickListener dequeueButtonListener, + boolean showResolverIcon) { + TextView trackNameTextView = (TextView) findViewById(R.id.track_textview); + trackNameTextView.setText(query.getPrettyName()); + setTextViewEnabled(trackNameTextView, query.isPlayable(), false); + + ImageView resolverImageView = (ImageView) ensureInflation(R.id.resolver_imageview_stub, + R.id.resolver_imageview); + TextView numerationTextView = (TextView) findViewById(R.id.numeration_textview); + if (showAsQueued) { + ImageView dequeueImageView = (ImageView) findViewById(R.id.dequeue_imageview); + if (dequeueButtonListener != null && dequeueImageView != null) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), dequeueImageView, + R.drawable.ic_navigation_close, R.color.tomahawk_red); + dequeueImageView.setOnClickListener(dequeueButtonListener); + } + } else if (showAsPlaying || showResolverIcon) { + if (numerationTextView != null) { + numerationTextView.setVisibility(View.GONE); + } + if (resolverImageView != null) { + resolverImageView.setVisibility(View.VISIBLE); + if (query.getPreferredTrackResult() != null) { + Resolver resolver = query.getPreferredTrackResult().getResolvedBy(); + resolver.loadIcon(resolverImageView, false); + } + } + } else if (numerationString != null) { + if (resolverImageView != null) { + resolverImageView.setVisibility(View.GONE); + } + if (numerationTextView != null) { + numerationTextView.setVisibility(View.VISIBLE); + numerationTextView.setText(numerationString); + setTextViewEnabled(numerationTextView, query.isPlayable(), false); + } + } + if (mLayoutId == R.layout.list_item_numeration_track_artist + || mLayoutId == R.layout.list_item_track_artist + || mLayoutId == R.layout.list_item_track_artist_queued) { + TextView artistNameTextView = (TextView) findViewById(R.id.artist_textview); + artistNameTextView.setText(query.getArtist().getPrettyName()); + setTextViewEnabled(artistNameTextView, query.isPlayable(), false); + } else if (mLayoutId == R.layout.list_item_numeration_track_duration) { + TextView durationTextView = (TextView) findViewById(R.id.duration_textview); + if (query.getPreferredTrack().getDuration() > 0) { + durationTextView.setText(ViewUtils.durationToString( + (query.getPreferredTrack().getDuration()))); + } else { + durationTextView.setText(PlaybackPanel.COMPLETION_STRING_DEFAULT); + } + setTextViewEnabled(durationTextView, query.isPlayable(), false); + } + } + + public void fillView(Track track) { + TextView trackNameTextView = (TextView) findViewById(R.id.track_textview); + trackNameTextView.setText(track.getName()); + TextView artistNameTextView = (TextView) findViewById(R.id.artist_textview); + artistNameTextView.setText(track.getArtist().getPrettyName()); + } + + public void fillView(String string) { + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(string); + } + + public void fillView(ListItemDrawable drawable) { + ImageView imageView = (ImageView) findViewById(R.id.imageview1); + imageView.setImageResource(drawable.getResourceId()); + } + + public void fillView(User user) { + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(user.getName()); + if (mLayoutId == R.layout.list_item_user) { + TextView textView2 = (TextView) findViewById(R.id.textview2); + if (user.getFollowersCount() >= 0 && user.getFollowCount() >= 0) { + textView2.setText(TomahawkApp.getContext().getString(R.string.followers_count, + user.getFollowersCount(), user.getFollowCount())); + } + } + TextView userTextView1 = (TextView) findViewById(R.id.usertextview1); + ImageView userImageView1 = (ImageView) findViewById(R.id.userimageview1); + ImageUtils.loadUserImageIntoImageView(TomahawkApp.getContext(), + userImageView1, user, Image.getSmallImageSize(), + userTextView1); + } + + public void fillView(Artist artist, String numerationString) { + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(artist.getPrettyName()); + if (numerationString != null) { + textView1.setText(numerationString + ": " + artist.getPrettyName()); + } else { + textView1.setText(artist.getPrettyName()); + } + ImageView imageView1 = (ImageView) findViewById(R.id.imageview1); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView1, + artist.getImage(), Image.getSmallImageSize(), true); + } + + public void fillView(final Album album, Collection collection, String numerationString) { + if (collection == null) { + collection = CollectionManager.get().getHatchetCollection(); + } + TextView textView1 = (TextView) findViewById(R.id.textview1); + if (numerationString != null) { + textView1.setText(numerationString + ": " + album.getPrettyName()); + } else { + textView1.setText(album.getPrettyName()); + } + TextView textView2 = (TextView) findViewById(R.id.textview2); + textView2.setText(album.getArtist().getPrettyName()); + ImageView imageView1 = (ImageView) findViewById(R.id.imageview1); + if (album.getImage() != null) { + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView1, + album.getImage(), Image.getSmallImageSize(), false); + } else { + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView1, + album.getArtist().getImage(), Image.getSmallImageSize(), false); + } + final TextView textView3 = (TextView) findViewById(R.id.textview3); + collection.getAlbumTrackCount(album).done(new DoneCallback() { + @Override + public void onDone(Integer trackCount) { + if (trackCount != null) { + textView3.setVisibility(View.VISIBLE); + textView3.setText(TomahawkApp.getContext().getResources().getQuantityString( + R.plurals.songs_with_count, trackCount, trackCount)); + } else { + textView3.setVisibility(View.INVISIBLE); + } + } + }); + } + + public void fillView(Resolver resolver) { + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(resolver.getPrettyName()); + ImageView imageView1 = (ImageView) findViewById(R.id.imageview1); + imageView1.clearColorFilter(); + if (!(resolver instanceof ScriptResolver) || + ((ScriptResolver) resolver).getScriptAccount().getMetaData() + .manifest.iconBackground != null) { + resolver.loadIconBackground(imageView1, !resolver.isEnabled()); + } else { + if (resolver.isEnabled()) { + imageView1.setBackgroundColor(TomahawkApp.getContext().getResources() + .getColor(android.R.color.black)); + } else { + imageView1.setBackgroundColor(TomahawkApp.getContext().getResources() + .getColor(R.color.fallback_resolver_bg)); + } + } + ImageView imageView2 = (ImageView) findViewById(R.id.imageview2); + if (!(resolver instanceof ScriptResolver) || + ((ScriptResolver) resolver).getScriptAccount().getMetaData() + .manifest.iconWhite != null) { + resolver.loadIconWhite(imageView2, 0); + } else { + resolver.loadIcon(imageView2, !resolver.isEnabled()); + } + View connectImageViewContainer = findViewById(R.id.connect_imageview); + if (resolver.isEnabled()) { + connectImageViewContainer.setVisibility(View.VISIBLE); + } else { + connectImageViewContainer.setVisibility(View.GONE); + } + } + + public void fillView(StationPlaylist playlist) { + ArrayList artistImages = new ArrayList<>(); + if (playlist.getArtists() != null) { + for (Pair pair : playlist.getArtists()) { + artistImages.add(pair.first.getImage()); + } + } + if (playlist.getTracks() != null) { + for (Pair pair : playlist.getTracks()) { + artistImages.add(pair.first.getArtist().getImage()); + } + } + if (playlist.getGenres() != null && artistImages.size() == 0) { + View v = ViewUtils.ensureInflation(mRootView, R.id.imageview_station_genre_stub, + R.id.imageview_station_genre); + v.setVisibility(View.VISIBLE); + v = mRootView.findViewById(R.id.imageview_grid_one); + if (v != null) { + v.setVisibility(View.GONE); + } + v = mRootView.findViewById(R.id.imageview_grid_two); + if (v != null) { + v.setVisibility(View.GONE); + } + v = mRootView.findViewById(R.id.imageview_grid_three); + if (v != null) { + v.setVisibility(View.GONE); + } + } else { + View v = mRootView.findViewById(R.id.imageview_station_genre); + if (v != null) { + v.setVisibility(View.GONE); + } + fillView(mRootView, artistImages, 0, false); + } + TextView textView1 = (TextView) findViewById(R.id.textview1); + if (textView1 != null) { + textView1.setText(playlist.getName()); + } + } + + public void fillView(Playlist playlist) { + ArrayList artistImages = new ArrayList<>(); + String topArtistsString = ""; + String[] artists = playlist.getTopArtistNames(); + if (artists != null) { + for (int i = 0; i < artists.length && i < 5 && artistImages.size() < 3; i++) { + Artist artist = Artist.get(artists[i]); + topArtistsString += artists[i]; + if (i != artists.length - 1) { + topArtistsString += ", "; + } + if (artist.getImage() != null) { + artistImages.add(artist.getImage()); + } + } + } + fillView(mRootView, artistImages, 0, false); + TextView textView1 = (TextView) findViewById(R.id.textview1); + if (textView1 != null) { + textView1.setText(playlist.getName()); + } + TextView textView2 = (TextView) findViewById(R.id.textview2); + if (textView2 != null) { + textView2.setText(topArtistsString); + textView2.setVisibility(View.VISIBLE); + } + TextView textView3 = (TextView) findViewById(R.id.textview3); + if (textView3 != null && playlist.getCount() >= 0) { + textView3.setVisibility(View.VISIBLE); + textView3.setText(TomahawkApp.getContext().getResources().getQuantityString( + R.plurals.songs_with_count, (int) playlist.getCount(), playlist.getCount())); + } + } + + public static void fillView(View view, Playlist playlist, int height, boolean isPagerFragment) { + ArrayList artistImages = new ArrayList<>(); + String[] artists = playlist.getTopArtistNames(); + if (artists != null) { + for (int i = 0; i < artists.length && i < 5 && artistImages.size() < 3; i++) { + Artist artist = Artist.get(artists[i]); + if (artist.getImage() != null) { + artistImages.add(artist.getImage()); + } + } + } + fillView(view, artistImages, height, isPagerFragment); + } + + private static void fillView(View view, List artistImages, int height, + boolean isPagerFragment) { + View v; + int gridOneResId = isPagerFragment ? R.id.imageview_grid_one_pager + : R.id.imageview_grid_one; + int gridTwoResId = isPagerFragment ? R.id.imageview_grid_two_pager + : R.id.imageview_grid_two; + int gridThreeResId = isPagerFragment ? R.id.imageview_grid_three_pager + : R.id.imageview_grid_three; + int gridOneStubId = isPagerFragment ? R.id.imageview_grid_one_pager_stub + : R.id.imageview_grid_one_stub; + int gridTwoStubId = isPagerFragment ? R.id.imageview_grid_two_pager_stub + : R.id.imageview_grid_two_stub; + int gridThreeStubId = isPagerFragment ? R.id.imageview_grid_three_pager_stub + : R.id.imageview_grid_three_stub; + if (artistImages.size() > 2) { + v = view.findViewById(gridOneResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = view.findViewById(gridTwoResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = ViewUtils.ensureInflation(view, gridThreeStubId, gridThreeResId); + v.setVisibility(View.VISIBLE); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), + artistImages.get(0), Image.getLargeImageSize(), false); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview2), + artistImages.get(1), Image.getSmallImageSize(), false); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview3), + artistImages.get(2), Image.getSmallImageSize(), false); + } else if (artistImages.size() > 1) { + v = view.findViewById(gridOneResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = view.findViewById(gridThreeResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = ViewUtils.ensureInflation(view, gridTwoStubId, gridTwoResId); + v.setVisibility(View.VISIBLE); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), + artistImages.get(0), Image.getLargeImageSize(), false); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview2), + artistImages.get(1), Image.getSmallImageSize(), false); + } else { + v = view.findViewById(gridTwoResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = view.findViewById(gridThreeResId); + if (v != null) { + v.setVisibility(View.GONE); + } + v = ViewUtils.ensureInflation(view, gridOneStubId, gridOneResId); + v.setVisibility(View.VISIBLE); + if (artistImages.size() > 0) { + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), + artistImages.get(0), Image.getLargeImageSize(), false); + } else { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), + R.drawable.album_placeholder); + } + } + if (height > 0) { + v.getLayoutParams().height = height; + } + } + + public void fillHeaderView(ArrayList spinnerItems, + int initialSelection, AdapterView.OnItemSelectedListener listener) { + ArrayAdapter adapter = + new ArrayAdapter<>(TomahawkApp.getContext(), + R.layout.dropdown_header_textview, spinnerItems); + adapter.setDropDownViewResource(R.layout.dropdown_header_dropdown_textview); + Spinner spinner = (Spinner) findViewById(R.id.spinner1); + spinner.setAdapter(adapter); + spinner.setSelection(initialSelection); + spinner.setOnItemSelectedListener(listener); + } + + public void fillHeaderView(String text) { + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(text); + } + + public void fillHeaderView(SocialAction socialAction, int segmentSize) { + ImageView userImageView1 = (ImageView) findViewById(R.id.userimageview1); + TextView userTextView = (TextView) findViewById(R.id.usertextview1); + ImageUtils.loadUserImageIntoImageView(TomahawkApp.getContext(), + userImageView1, socialAction.getUser(), + Image.getSmallImageSize(), userTextView); + Object targetObject = socialAction.getTargetObject(); + Resources resources = TomahawkApp.getContext().getResources(); + String userName = socialAction.getUser().getName(); + String phrase = "!FIXME! type: " + socialAction.getType() + + ", action: " + socialAction.getAction() + ", user: " + userName; + if (HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_LOVE + .equals(socialAction.getType())) { + if (targetObject instanceof Query) { + phrase = resources.getQuantityString(R.plurals.socialaction_type_love_track, + segmentSize, userName, segmentSize); + } else if (targetObject instanceof Album) { + phrase = resources.getQuantityString(R.plurals.socialaction_type_collected_album, + segmentSize, userName, segmentSize); + } else if (targetObject instanceof Artist) { + phrase = resources.getQuantityString(R.plurals.socialaction_type_collected_artist, + segmentSize, userName, segmentSize); + } + } else if (HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_FOLLOW + .equals(socialAction.getType())) { + phrase = resources.getString(R.string.socialaction_type_follow, userName); + } else if (HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_CREATEPLAYLIST + .equals(socialAction.getType())) { + phrase = resources.getQuantityString(R.plurals.socialaction_type_createplaylist, + segmentSize, userName, segmentSize); + } else if (HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_LATCHON + .equals(socialAction.getType())) { + phrase = resources.getQuantityString(R.plurals.socialaction_type_latchon, + segmentSize, userName, segmentSize); + } + TextView textView1 = (TextView) findViewById(R.id.textview1); + textView1.setText(phrase + ":"); + } + + private static String dateToString(Resources resources, Date date) { + String s = ""; + if (date != null) { + long diff = System.currentTimeMillis() - date.getTime(); + if (diff < 60000) { + s += resources.getString(R.string.time_afewseconds); + } else if (diff < 3600000) { + long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); + s += resources.getQuantityString(R.plurals.time_minute, (int) minutes, minutes); + } else if (diff < 86400000) { + long hours = TimeUnit.MILLISECONDS.toHours(diff); + s += resources.getQuantityString(R.plurals.time_hour, (int) hours, hours); + } else { + long days = TimeUnit.MILLISECONDS.toDays(diff); + s += resources.getQuantityString(R.plurals.time_day, (int) days, days); + } + } + return s; + } + + private static void setTextViewEnabled(TextView textView, boolean enabled, + boolean isSecondary) { + if (textView != null && textView.getResources() != null) { + int colorResId; + if (enabled) { + if (isSecondary) { + colorResId = R.color.secondary_textcolor; + } else { + colorResId = R.color.primary_textcolor; + } + } else { + colorResId = R.color.disabled; + } + textView.setTextColor(textView.getResources().getColor(colorResId)); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/AskAccessConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/AskAccessConfigDialog.java new file mode 100644 index 000000000..5d0227441 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/AskAccessConfigDialog.java @@ -0,0 +1,75 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.resolver.HatchetStubResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.widget.Toast; + +/** + * A {@link android.support.v4.app.DialogFragment} which is being shown to the user to ask him to + * give us the notification listener permission, so that we can also scrobble to hatchet when music + * is being played in other apps. + */ +public class AskAccessConfigDialog extends ConfigDialog { + + public final static String TAG = AskAccessConfigDialog.class.getSimpleName(); + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + addScrollingViewToFrame(R.layout.config_ask_access); + setDialogTitle(HatchetAuthenticatorUtils.HATCHET_PRETTY_NAME); + onResolverStateUpdated(HatchetStubResolver.get()); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + try { + startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } catch (ActivityNotFoundException e) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(TomahawkApp.getContext(), + R.string.notification_settings_activity_not_found, Toast.LENGTH_LONG) + .show(); + } + }); + } + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ConfigDialog.java new file mode 100644 index 000000000..96e21624e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ConfigDialog.java @@ -0,0 +1,277 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; + +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import de.greenrobot.event.EventBus; +import fr.castorflex.android.smoothprogressbar.SmoothProgressBar; + +/** + * A {@link android.support.v4.app.DialogFragment} which is the base class for all config dialogs + * (ResolverConfigDialog, RedirectConfigDialog, LoginConfigDialog) + */ +public abstract class ConfigDialog extends DialogFragment { + + public final static String TAG = ConfigDialog.class.getSimpleName(); + + private View mDialogView; + + private ImageView mHeaderBackground; + + private TextView mTitleTextView; + + private LinearLayout mScrollingDialogFrame; + + private LinearLayout mDialogFrame; + + private TextView mPositiveButton; + + private TextView mNegativeButton; + + private ImageView mStatusImageView; + + private ImageView mRemoveButton; + + protected SmoothProgressBar mProgressBar; + + private Resolver mResolver; + + //So that the user can login by pressing "Enter" or something similar on his keyboard + protected final TextView.OnEditorActionListener mOnKeyboardEnterListener + = new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (event == null || actionId == EditorInfo.IME_ACTION_SEARCH + || actionId == EditorInfo.IME_ACTION_DONE + || event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + onPositiveAction(); + } + return false; + } + }; + + private final View.OnClickListener mPositiveButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onPositiveAction(); + } + }; + + private final View.OnClickListener mNegativeButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onNegativeAction(); + } + }; + + @SuppressWarnings("unused") + public void onEventMainThread(AuthenticatorManager.ConfigTestResultEvent event) { + onResolverStateUpdated(mResolver); + onConfigTestResult(event.mComponent, event.mType, event.mMessage); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + LayoutInflater inflater = getActivity().getLayoutInflater(); + mDialogView = inflater.inflate(R.layout.config_dialog, null); + mHeaderBackground = (ImageView) mDialogView + .findViewById(R.id.config_dialog_header_background); + mTitleTextView = (TextView) mDialogView + .findViewById(R.id.config_dialog_title_textview); + mDialogFrame = (LinearLayout) mDialogView + .findViewById(R.id.config_dialog_frame); + mScrollingDialogFrame = (LinearLayout) mDialogView + .findViewById(R.id.scrolling_config_dialog_frame); + mPositiveButton = (TextView) mDialogView + .findViewById(R.id.config_dialog_positive_button); + mPositiveButton.setOnClickListener(mPositiveButtonListener); + mNegativeButton = (TextView) mDialogView + .findViewById(R.id.config_dialog_negative_button); + mNegativeButton.setOnClickListener(mNegativeButtonListener); + mStatusImageView = (ImageView) mDialogView + .findViewById(R.id.config_dialog_status_imageview); + mProgressBar = (SmoothProgressBar) mDialogView + .findViewById(R.id.smoothprogressbar); + mRemoveButton = (ImageView) mDialogView + .findViewById(R.id.config_dialog_remove_button); + + mPositiveButton.setText(getString(R.string.ok).toUpperCase()); + mNegativeButton.setText(getString(R.string.cancel).toUpperCase()); + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + + super.onStop(); + } + + public View getDialogView() { + return mDialogView; + } + + protected void addScrollingViewToFrame(View view) { + mScrollingDialogFrame.addView(view); + } + + protected View addScrollingViewToFrame(int layoutId) { + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(layoutId, mScrollingDialogFrame, false); + mScrollingDialogFrame.addView(view); + return view; + } + + protected View addViewToFrame(int layoutId) { + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(layoutId, mScrollingDialogFrame, false); + mDialogFrame.addView(view); + mDialogFrame.setVisibility(View.VISIBLE); + return view; + } + + protected void onConfigTestResult(Object component, int type, String message) { + } + + protected abstract void onPositiveAction(); + + private void onNegativeAction() { + dismiss(); + } + + protected void hideNegativeButton() { + mNegativeButton.setVisibility(View.GONE); + } + + protected void onResolverStateUpdated(Resolver resolver) { + mResolver = resolver; + if (!(resolver instanceof ScriptResolver) || + ((ScriptResolver) resolver).getScriptAccount().getMetaData() + .manifest.iconBackground != null) { + resolver.loadIconBackground(mHeaderBackground, !resolver.isEnabled()); + } else { + int color; + if (resolver.isEnabled()) { + color = android.R.color.black; + } else { + color = R.color.disabled_resolver; + } + mHeaderBackground.setImageDrawable(new ColorDrawable(getResources().getColor(color))); + } + if (!(resolver instanceof ScriptResolver) || + ((ScriptResolver) resolver).getScriptAccount().getMetaData() + .manifest.iconWhite != null) { + resolver.loadIconWhite(mStatusImageView, 0); + } else { + resolver.loadIcon(mStatusImageView, false); + } + + View button = getDialogView().findViewById(R.id.config_enable_button); + if (button != null) { + ImageView buttonImage = + (ImageView) button.findViewById(R.id.config_enable_button_image); + TextView buttonText = (TextView) button.findViewById(R.id.config_enable_button_text); + if (resolver.isEnabled()) { + button.setBackgroundResource(R.drawable.selectable_background_tomahawk_red_filled); + resolver.loadIconWhite(buttonImage, 0); + buttonText.setText(R.string.resolver_config_enable_button_disable); + buttonText.setTextColor( + getResources().getColor(R.color.primary_textcolor_inverted)); + } else { + button.setBackgroundResource(R.drawable.selectable_background_tomahawk_red); + resolver.loadIconWhite(buttonImage, R.color.tomahawk_red); + buttonText.setText(R.string.resolver_config_enable_button_enable); + buttonText.setTextColor(getResources().getColor(R.color.tomahawk_red)); + } + } + } + + protected void showEnableButton(View.OnClickListener onClickListener) { + View button = addScrollingViewToFrame(R.layout.config_enable_button); + button.setOnClickListener(onClickListener); + } + + protected void showRemoveButton(View.OnClickListener onClickListener) { + ImageUtils.setTint(mRemoveButton.getDrawable(), R.color.tomahawk_red); + mRemoveButton.setVisibility(View.VISIBLE); + mRemoveButton.setOnClickListener(onClickListener); + } + + protected void setStatusImage(int drawableResId) { + mStatusImageView.setImageResource(drawableResId); + } + + protected void setDialogTitle(String title) { + if (mTitleTextView != null) { + mTitleTextView.setText(title); + } + } + + protected void setPositiveButtonText(int stringResId) { + if (mPositiveButton != null) { + String string = getString(stringResId).toUpperCase(); + mPositiveButton.setText(string); + } + } + + protected void setNegativeButtonText(int stringResId) { + if (mNegativeButton != null) { + String string = getString(stringResId).toUpperCase(); + mNegativeButton.setText(string); + } + } + + /** + * Start the loading animation. Called when beginning login process. + */ + public void startLoadingAnimation() { + mProgressBar.setVisibility(View.VISIBLE); + } + + /** + * Stop the loading animation. Called when login/logout process has finished. + */ + public void stopLoadingAnimation() { + mProgressBar.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreatePlaylistDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreatePlaylistDialog.java new file mode 100644 index 000000000..63135a080 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreatePlaylistDialog.java @@ -0,0 +1,119 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.fragments.ContentHeaderFragment; +import org.tomahawk.tomahawk_android.fragments.PlaylistEntriesFragment; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigEdittext; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.text.TextUtils; +import android.widget.EditText; + +/** + * A {@link DialogFragment} which is presented for the user so that he can choose a name for the + * {@link org.tomahawk.libtomahawk.collection.Playlist} he intends to create + */ +public class CreatePlaylistDialog extends ConfigDialog { + + private User mUser; + + private Playlist mPlaylist; + + private EditText mNameEditText; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Check if there is a playlist key in the provided arguments + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.PLAYLIST)) { + String playlistId = getArguments().getString(TomahawkFragment.PLAYLIST); + mPlaylist = DatabaseHelper.get().getPlaylist(playlistId); + if (mPlaylist == null) { + mPlaylist = Playlist.getByKey(playlistId); + if (mPlaylist == null) { + dismiss(); + } + } + } + if (getArguments().containsKey(TomahawkFragment.USER)) { + mUser = User.getUserById(getArguments().getString(TomahawkFragment.USER)); + if (mUser == null) { + dismiss(); + } + } + } + + //set the proper flags for our edittext + mNameEditText = (ConfigEdittext) addScrollingViewToFrame(R.layout.config_edittext); + mNameEditText.setHint(R.string.name_playlist); + mNameEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + + ViewUtils.showSoftKeyboard(mNameEditText); + + //Set the textview's text to the proper title + setDialogTitle(getString(R.string.create_playlist)); + + setStatusImage(R.drawable.ic_action_playlist); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + /** + * Persist a {@link org.tomahawk.libtomahawk.collection.Playlist} as a {@link + * org.tomahawk.libtomahawk.collection.Playlist} in our database + */ + private void savePlaylist() { + if (mPlaylist != null) { + String playlistName = TextUtils.isEmpty(mNameEditText.getText().toString()) + ? getString(R.string.playlist) + : mNameEditText.getText().toString(); + mPlaylist.setName(playlistName); + CollectionManager.get().createPlaylist(mPlaylist); + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + bundle.putString(TomahawkFragment.PLAYLIST, mPlaylist.getCacheKey()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + PlaylistEntriesFragment.class, bundle); + } + } + + @Override + protected void onPositiveAction() { + savePlaylist(); + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreateStationDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreateStationDialog.java new file mode 100644 index 000000000..ddedf5686 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/CreateStationDialog.java @@ -0,0 +1,201 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.ListItemString; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGenerator; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorManager; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorSearchResult; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigEdittext; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +/** + * A {@link ConfigDialog} which shows a textfield to enter a username and password, and provides + * button for cancel/logout and ok/login, depending on whether or not the user is logged in. + */ +public class CreateStationDialog extends ConfigDialog { + + public final static String TAG = CreateStationDialog.class.getSimpleName(); + + private TomahawkListAdapter mAdapter; + + private StickyListHeadersListView mListView; + + private EditText mSearchEditText; + + private Map mArtistIds = new ConcurrentHashMap<>(); + + private Map mTrackIds = new ConcurrentHashMap<>(); + + private ClickListener mClickListener = new ClickListener(); + + private class ClickListener implements MultiColumnClickListener { + + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (item instanceof Artist) { + List> artists = new ArrayList<>(); + artists.add(new Pair<>((Artist) item, mArtistIds.get(item))); + StationPlaylist stationPlaylist = StationPlaylist.get(artists, null, null); + DatabaseHelper.get().storeStation(stationPlaylist); + } else if (item instanceof Track) { + List> tracks = new ArrayList<>(); + tracks.add(new Pair<>((Track) item, mTrackIds.get(item))); + StationPlaylist stationPlaylist = StationPlaylist.get(null, tracks, null); + DatabaseHelper.get().storeStation(stationPlaylist); + } else if (item instanceof ListItemString) { + List genres = new ArrayList<>(); + genres.add(((ListItemString) item).getText()); + StationPlaylist stationPlaylist = StationPlaylist.get(null, null, genres); + DatabaseHelper.get().storeStation(stationPlaylist); + } + CreateStationDialog.this.dismiss(); + } + + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + return false; + } + } + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View layout = addViewToFrame(R.layout.config_create_station); + mListView = + (StickyListHeadersListView) layout.findViewById(R.id.create_station_listview); + mSearchEditText = (ConfigEdittext) layout.findViewById(R.id.create_station_edittext); + mSearchEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + + ViewUtils.showSoftKeyboard(mSearchEditText); + + setDialogTitle(getString(R.string.create_station)); + + setStatusImage(R.drawable.ic_action_station); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + mListView.setVisibility(View.GONE); + startLoadingAnimation(); + ScriptPlaylistGenerator generator = + ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator(); + if (generator != null) { + generator.search(mSearchEditText.getText().toString()) + .done(new DoneCallback() { + @Override + public void onDone(ScriptPlaylistGeneratorSearchResult result) { + stopLoadingAnimation(); + mListView.setVisibility(View.VISIBLE); + List segments = new ArrayList<>(); + if (result.mArtists.size() > 0) { + List artists = new ArrayList<>(); + for (Pair pair : result.mArtists) { + artists.add(pair.first); + mArtistIds.put(pair.first, pair.second); + } + segments.add(new Segment.Builder(artists) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.artists) + .build()); + } + if (result.mAlbums.size() > 0) { + segments.add(new Segment.Builder(result.mAlbums) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.albums) + .build()); + } + if (result.mTracks.size() > 0) { + List tracks = new ArrayList<>(); + for (Pair pair : result.mTracks) { + tracks.add(pair.first); + mTrackIds.put(pair.first, pair.second); + } + segments.add(new Segment.Builder(tracks) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.songs) + .build()); + } + if (result.mGenres.size() > 0) { + List genres = new ArrayList<>(); + for (String genre : result.mGenres) { + genres.add(new ListItemString(genre)); + } + segments.add(new Segment.Builder(genres) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.genres) + .build()); + } + if (result.mMoods.size() > 0) { + List moods = new ArrayList<>(); + for (String mood : result.mMoods) { + moods.add(new ListItemString(mood)); + } + segments.add(new Segment.Builder(moods) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.moods) + .build()); + } + + if (mAdapter == null) { + mAdapter = new TomahawkListAdapter( + (TomahawkMainActivity) getActivity(), + LayoutInflater.from(getContext()), segments, mListView, + mClickListener); + } else { + mAdapter.setSegments(segments, mListView); + } + mListView.setAdapter(mAdapter); + } + }); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/DirectoryChooserConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/DirectoryChooserConfigDialog.java new file mode 100644 index 000000000..6d0da9c86 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/DirectoryChooserConfigDialog.java @@ -0,0 +1,62 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.resolver.UserCollectionStubResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.views.DirectoryChooser; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; + +/** + * A {@link org.tomahawk.tomahawk_android.dialogs.ConfigDialog} which shows a textfield to enter a + * username and password, and provides button for cancel/logout and ok/login, depending on whether + * or not the user is logged in. + */ +public class DirectoryChooserConfigDialog extends ConfigDialog { + + public final static String TAG = DirectoryChooserConfigDialog.class.getSimpleName(); + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DirectoryChooser directoryChooser = + (DirectoryChooser) addViewToFrame(R.layout.config_directorychooser); + directoryChooser.setup(); + + setDialogTitle(getString(R.string.local_collection_pretty_name)); + onResolverStateUpdated(UserCollectionStubResolver.get()); + setPositiveButtonText(R.string.rescan); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + CollectionManager.get().getUserCollection().loadMediaItems(true); + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/GMusicConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/GMusicConfigDialog.java new file mode 100644 index 000000000..f072ef7d0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/GMusicConfigDialog.java @@ -0,0 +1,207 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.google.android.gms.auth.GooglePlayServicesAvailabilityException; +import com.google.android.gms.auth.UserRecoverableAuthException; +import com.google.android.gms.common.GoogleApiAvailability; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link android.support.v4.app.DialogFragment} which redirects the user to an external login + * activity. + */ +public class GMusicConfigDialog extends ConfigDialog { + + public final static String TAG = GMusicConfigDialog.class.getSimpleName(); + + public final static int REQUEST_CODE_PLAY_SERVICES_ERROR = 12; + + public final static int REQUEST_CODE_RECOVERABLE_ERROR = 13; + + private ScriptResolver mScriptResolver; + + private RadioGroup mRadioGroup; + + private Map mAccountMap; + + public static class ActivityResultEvent { + + public int requestCode; + + public int resultCode; + } + + @SuppressWarnings("unused") + public void onEvent(ActivityResultEvent event) { + if (event.resultCode == Activity.RESULT_OK) { + final Account account = mAccountMap.get(mRadioGroup.getCheckedRadioButtonId()); + + if (account != null) { + Log.d(TAG, "Account " + account.name + " selected. Getting auth token ..."); + fetchToken(account); + } else { + Log.e(TAG, "Account was null"); + } + } else { + Log.d(TAG, "ActivityResult was not RESULT_OK. Aborting ..."); + stopLoadingAnimation(); + } + } + + private View.OnClickListener mEnableButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!mScriptResolver.isEnabled()) { + final Account account = mAccountMap.get(mRadioGroup.getCheckedRadioButtonId()); + + if (account != null) { + Log.d(TAG, "Account " + account.name + " selected. Getting auth token ..."); + startLoadingAnimation(); + fetchToken(account); + } else { + Log.e(TAG, "Account was null"); + } + } else { + mScriptResolver.setEnabled(false); + onResolverStateUpdated(mScriptResolver); + } + } + }; + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mScriptResolver = PipeLine.get().getResolver(TomahawkApp.PLUGINNAME_GMUSIC); + + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(mScriptResolver.getDescription()); + + TextView infoTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + infoTextView.setText(R.string.gmusic_info_text); + + String loggedInAccountName = null; + Map config = mScriptResolver.getConfig(); + if (config.get("email") instanceof String) { + loggedInAccountName = (String) config.get("email"); + } + + mRadioGroup = (RadioGroup) addScrollingViewToFrame(R.layout.config_radiogroup); + final AccountManager accountManager = AccountManager.get(TomahawkApp.getContext()); + final Account[] accounts = accountManager.getAccountsByType("com.google"); + mAccountMap = new HashMap<>(); + LayoutInflater inflater = getActivity().getLayoutInflater(); + for (Account account : accounts) { + RadioButton radioButton = + (RadioButton) inflater.inflate(R.layout.config_radiobutton, mRadioGroup, false); + radioButton.setText(account.name); + mRadioGroup.addView(radioButton); + mAccountMap.put(radioButton.getId(), account); + if (loggedInAccountName != null && account.name.equals(loggedInAccountName)) { + mRadioGroup.check(radioButton.getId()); + } + } + showEnableButton(mEnableButtonListener); + onResolverStateUpdated(mScriptResolver); + + hideNegativeButton(); + + setDialogTitle(mScriptResolver.getName()); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onConfigTestResult(Object component, int type, String message) { + if (mScriptResolver == component) { + mScriptResolver.setEnabled( + type == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS); + onResolverStateUpdated(mScriptResolver); + stopLoadingAnimation(); + } + } + + @Override + protected void onPositiveAction() { + dismiss(); + } + + private void fetchToken(final Account account) { + new Thread(new Runnable() { + @Override + public void run() { + try { + String authToken = + GoogleAuthUtil.getToken(TomahawkApp.getContext(), account, "sj"); + Log.d(TAG, "Received auth token!"); + Map config = mScriptResolver.getConfig(); + config.put("token", authToken); + config.put("email", account.name); + mScriptResolver.setConfig(config); + mScriptResolver.testConfig(config); + } catch (GooglePlayServicesAvailabilityException e) { + Log.d(TAG, "GooglePlayServicesAvailabilityException: " + + e.getLocalizedMessage()); + if (getActivity() != null) { + GoogleApiAvailability.getInstance().showErrorDialogFragment(getActivity(), + e.getConnectionStatusCode(), REQUEST_CODE_PLAY_SERVICES_ERROR); + } + } catch (UserRecoverableAuthException e) { + Log.d(TAG, "UserRecoverableAuthException: " + e.getLocalizedMessage()); + if (getActivity() != null) { + getActivity().startActivityForResult(e.getIntent(), + REQUEST_CODE_RECOVERABLE_ERROR); + } + } catch (GoogleAuthException e) { + Log.d(TAG, "GoogleAuthException: " + e.getLocalizedMessage()); + } catch (IOException e) { + Log.d(TAG, "IOException: " + e.getLocalizedMessage()); + } + } + }).start(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/HatchetLoginDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/HatchetLoginDialog.java new file mode 100644 index 000000000..898daecab --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/HatchetLoginDialog.java @@ -0,0 +1,95 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.resolver.HatchetStubResolver; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.views.HatchetLoginRegisterView; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.WindowManager; +import android.widget.TextView; + +/** + * A {@link org.tomahawk.tomahawk_android.dialogs.ConfigDialog} which shows a textfield to enter a + * username and password, and provides button for cancel/logout and ok/login, depending on whether + * or not the user is logged in. + */ +public class HatchetLoginDialog extends ConfigDialog { + + public final static String TAG = HatchetLoginDialog.class.getSimpleName(); + + private AuthenticatorUtils mAuthenticatorUtils; + + private HatchetLoginRegisterView mHatchetLoginRegisterView; + + /** + * Called when this {@link DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getArguments() != null && getArguments() + .containsKey(TomahawkFragment.PREFERENCEID)) { + String authenticatorId = getArguments().getString( + TomahawkFragment.PREFERENCEID); + mAuthenticatorUtils = AuthenticatorManager.get().getAuthenticatorUtils( + authenticatorId); + } + + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(mAuthenticatorUtils.getDescription()); + mHatchetLoginRegisterView = (HatchetLoginRegisterView) addScrollingViewToFrame( + R.layout.config_hatchetloginregister); + mHatchetLoginRegisterView.setup(mAuthenticatorUtils, mProgressBar); + + setDialogTitle(mAuthenticatorUtils.getPrettyName()); + if (TomahawkApp.PLUGINNAME_HATCHET.equals(mAuthenticatorUtils.getId())) { + onResolverStateUpdated(HatchetStubResolver.get()); + } else { + onResolverStateUpdated(PipeLine.get().getResolver(mAuthenticatorUtils.getId())); + } + hideNegativeButton(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + alertDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + return alertDialog; + } + + @Override + protected void onConfigTestResult(Object component, int type, String message) { + mHatchetLoginRegisterView.onConfigTestResult(component, type, message); + } + + @Override + protected void onPositiveAction() { + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/InstallPluginConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/InstallPluginConfigDialog.java new file mode 100644 index 000000000..a59be70c0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/InstallPluginConfigDialog.java @@ -0,0 +1,116 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FileUtils; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptAccount; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverMetaData; +import org.tomahawk.libtomahawk.utils.GsonHelper; +import org.tomahawk.libtomahawk.utils.VariousUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.UnzipUtils; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class InstallPluginConfigDialog extends ConfigDialog { + + public final static String TAG = InstallPluginConfigDialog.class.getSimpleName(); + + public static final String PATH_TO_AXE_URI_STRING = "path_to_axe_uri_string"; + + private Uri mPathToAxe; + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Check if there is a data key in the provided arguments + if (getArguments() != null && getArguments().containsKey(PATH_TO_AXE_URI_STRING)) { + mPathToAxe = Uri.parse(getArguments().getString(PATH_TO_AXE_URI_STRING)); + } + + addScrollingViewToFrame(R.layout.config_install_plugin); + setDialogTitle(getString(R.string.install_plugin_title)); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + new Thread(new Runnable() { + @Override + public void run() { + String destDirPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + + File.separator + "manualresolvers" + File.separator + ".temp"; + File destDir = new File(destDirPath); + try { + VariousUtils.deleteRecursive(destDir); + } catch (FileNotFoundException e) { + Log.d(TAG, + "onPositiveAction: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + try { + if (UnzipUtils.unzip(mPathToAxe, destDirPath)) { + File metadataFile = new File(destDirPath + File.separator + "content" + + File.separator + "metadata.json"); + String metadataString = FileUtils.readFileToString(metadataFile, + Charsets.UTF_8); + ScriptResolverMetaData metaData = GsonHelper.get().fromJson(metadataString, + ScriptResolverMetaData.class); + final File renamedFile = new File( + destDir.getParent() + File.separator + metaData.pluginName + + "_" + System.currentTimeMillis()); + boolean success = destDir.renameTo(renamedFile); + if (!success) { + Log.e(TAG, "onPositiveAction - Wasn't able to rename directory: " + + renamedFile.getAbsolutePath()); + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + PipeLine.get().addScriptAccount( + new ScriptAccount(renamedFile.getPath(), true)); + } + }); + } + } catch (IOException e) { + Log.e(TAG, + "onPositiveAction: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + }).start(); + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/RemovePluginConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/RemovePluginConfigDialog.java new file mode 100644 index 000000000..584ff589d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/RemovePluginConfigDialog.java @@ -0,0 +1,75 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.utils.VariousUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.widget.TextView; + +import java.io.File; +import java.io.FileNotFoundException; + +public class RemovePluginConfigDialog extends ConfigDialog { + + public final static String TAG = RemovePluginConfigDialog.class.getSimpleName(); + + private ScriptResolver mScriptResolver; + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getArguments() != null && getArguments().containsKey(TomahawkFragment.PREFERENCEID)) { + String resolverId = getArguments().getString( + TomahawkFragment.PREFERENCEID); + mScriptResolver = PipeLine.get().getResolver(resolverId); + } + + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(R.string.uninstall_plugin_warning); + setDialogTitle(getString(R.string.uninstall_plugin_title)); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + File destDir = + new File(mScriptResolver.getScriptAccount().getPath().replaceFirst("file:", "")); + try { + VariousUtils.deleteRecursive(destDir); + mScriptResolver.getScriptAccount().unregisterAllPlugins(); + } catch (FileNotFoundException e) { + Log.d(TAG, "onPositiveAction: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + PipeLine.get().removeResolver(mScriptResolver); + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverConfigDialog.java new file mode 100644 index 000000000..fa16959e4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverConfigDialog.java @@ -0,0 +1,220 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverConfigUiField; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigCheckbox; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigDropDown; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigEdittext; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigFieldView; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.Html; +import android.text.InputType; +import android.text.method.LinkMovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A {@link android.support.v4.app.DialogFragment} which shows checkboxes and edittexts depending on + * the given ScriptResolver's config. Enables the user to configure a certain ScriptResolver. + */ +public class ResolverConfigDialog extends ConfigDialog { + + public final static String TAG = ResolverConfigDialog.class.getSimpleName(); + + private ScriptResolver mScriptResolver; + + private final ArrayList mConfigFieldViews = new ArrayList<>(); + + private View.OnClickListener mEnableButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!mScriptResolver.isEnabled()) { + saveConfig(); + } else { + mScriptResolver.setEnabled(false); + } + onResolverStateUpdated(mScriptResolver); + } + }; + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getArguments() != null && getArguments().containsKey(TomahawkFragment.PREFERENCEID)) { + String resolverId = getArguments().getString(TomahawkFragment.PREFERENCEID); + mScriptResolver = PipeLine.get().getResolver(resolverId); + } + + EditText showKeyboardEditText = null; + EditText lastEditText = null; + if (mScriptResolver.getConfigUi() != null) { + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(mScriptResolver.getDescription()); + for (ScriptResolverConfigUiField field : mScriptResolver.getConfigUi()) { + Map config = mScriptResolver.getConfig(); + if (ScriptResolverConfigUiField.TYPE_TEXTVIEW.equals(field.type)) { + TextView textView = + (TextView) addScrollingViewToFrame(R.layout.config_textview); + if (field.text.startsWith("")) { + textView.setText(Html.fromHtml(field.text)); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + textView.setText(field.text); + } + } else if (ScriptResolverConfigUiField.TYPE_CHECKBOX.equals(field.type)) { + LinearLayout checkboxLayout = + (LinearLayout) addScrollingViewToFrame(R.layout.config_checkbox); + TextView textView = + (TextView) checkboxLayout.findViewById(R.id.config_textview); + textView.setText(field.label); + ConfigCheckbox checkBox = + (ConfigCheckbox) checkboxLayout.findViewById(R.id.config_checkbox); + checkBox.mConfigFieldId = field.id; + mConfigFieldViews.add(checkBox); + if (config.get(field.id) != null) { + checkBox.setChecked((Boolean) config.get(field.id)); + } else { + checkBox.setChecked(Boolean.valueOf(field.defaultValue)); + } + } else if (ScriptResolverConfigUiField.TYPE_TEXTFIELD.equals(field.type)) { + ConfigEdittext editText = + (ConfigEdittext) addScrollingViewToFrame(R.layout.config_edittext); + editText.mConfigFieldId = field.id; + editText.setHint(field.label); + mConfigFieldViews.add(editText); + if (config.get(field.id) != null) { + editText.setText((String) config.get(field.id)); + } else { + editText.setText(field.defaultValue); + } + if (field.isPassword) { + editText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + editText.setTransformationMethod(new PasswordTransformationMethod()); + } + if (showKeyboardEditText == null) { + showKeyboardEditText = editText; + } + lastEditText = editText; + } else if (ScriptResolverConfigUiField.TYPE_DROPDOWN.equals(field.type)) { + LinearLayout numberpickerLayout = + (LinearLayout) addScrollingViewToFrame(R.layout.config_dropdown); + TextView textView = + (TextView) numberpickerLayout.findViewById(R.id.config_textview); + textView.setText(field.label); + ConfigDropDown dropDown = + (ConfigDropDown) numberpickerLayout.findViewById(R.id.config_dropdown); + dropDown.mConfigFieldId = field.id; + mConfigFieldViews.add(dropDown); + List list = new ArrayList<>(); + for (String item : field.items) { + list.add(item); + } + ArrayAdapter adapter = new ArrayAdapter<>( + TomahawkApp.getContext(), R.layout.spinner_textview, list); + adapter.setDropDownViewResource(R.layout.spinner_dropdown_textview); + dropDown.setAdapter(adapter); + if (config.get(field.id) != null) { + dropDown.setSelection(((Double) config.get(field.id)).intValue()); + } else { + dropDown.setSelection(Integer.valueOf(field.defaultValue)); + } + } + } + } + if (mScriptResolver.getScriptAccount().isManuallyInstalled()) { + showRemoveButton(new View.OnClickListener() { + @Override + public void onClick(View v) { + RemovePluginConfigDialog dialog = new RemovePluginConfigDialog(); + Bundle args = new Bundle(); + args.putString(TomahawkFragment.PREFERENCEID, mScriptResolver.getId()); + dialog.setArguments(args); + dialog.show(getFragmentManager(), null); + dismiss(); + } + }); + } + if (lastEditText != null) { + lastEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + } + if (showKeyboardEditText != null) { + ViewUtils.showSoftKeyboard(showKeyboardEditText); + } + setDialogTitle(mScriptResolver.getName()); + + showEnableButton(mEnableButtonListener); + onResolverStateUpdated(mScriptResolver); + + hideNegativeButton(); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + /** + * Save the config. + */ + public void saveConfig() { + Map config = mScriptResolver.getConfig(); + for (ConfigFieldView configFieldView : mConfigFieldViews) { + config.put(configFieldView.getConfigFieldId(), configFieldView.getValue()); + } + mScriptResolver.setConfig(config); + mScriptResolver.testConfig(config); + startLoadingAnimation(); + } + + @Override + protected void onConfigTestResult(Object component, int type, String message) { + if (mScriptResolver == component) { + mScriptResolver.setEnabled( + type == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS); + onResolverStateUpdated(mScriptResolver); + stopLoadingAnimation(); + } + } + + @Override + protected void onPositiveAction() { + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverRedirectConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverRedirectConfigDialog.java new file mode 100644 index 000000000..9f0eb2807 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/ResolverRedirectConfigDialog.java @@ -0,0 +1,168 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.mediaplayers.DeezerMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.SpotifyMediaPlayer; +import org.tomahawk.tomahawk_android.utils.PluginUtils; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class ResolverRedirectConfigDialog extends ConfigDialog { + + public final static String TAG = ResolverRedirectConfigDialog.class.getSimpleName(); + + private ScriptResolver mScriptResolver; + + private TextView mRedirectButtonTextView; + + private TextView mWarningTextView; + + private class RedirectButtonListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + startLoadingAnimation(); + if (PluginUtils.isPluginUpToDate(mScriptResolver.getId())) { + if (mScriptResolver.isEnabled()) { + mScriptResolver.logout(); + } else { + mScriptResolver.login(); + } + } else { + String url = null; + boolean isPlayStoreInstalled = PluginUtils.isPlayStoreInstalled(); + switch (mScriptResolver.getId()) { + case TomahawkApp.PLUGINNAME_SPOTIFY: + if (isPlayStoreInstalled) { + url = "market://details?id=" + SpotifyMediaPlayer.PACKAGE_NAME; + } else { + url = SpotifyMediaPlayer.getPluginDownloadLink(); + } + break; + case TomahawkApp.PLUGINNAME_DEEZER: + if (isPlayStoreInstalled) { + url = "market://details?id=" + DeezerMediaPlayer.PACKAGE_NAME; + } else { + url = DeezerMediaPlayer.getPluginDownloadLink(); + } + break; + } + if (url != null) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + } + } + } + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getArguments() != null && getArguments().containsKey(TomahawkFragment.PREFERENCEID)) { + String id = getArguments().getString(TomahawkFragment.PREFERENCEID); + mScriptResolver = PipeLine.get().getResolver(id); + } + + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(mScriptResolver.getDescription()); + + mWarningTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + + View buttonLayout = addScrollingViewToFrame(R.layout.config_enable_button); + LinearLayout button = (LinearLayout) buttonLayout.findViewById(R.id.config_enable_button); + button.setOnClickListener(new RedirectButtonListener()); + ImageView buttonImage = + (ImageView) buttonLayout.findViewById(R.id.config_enable_button_image); + mScriptResolver.loadIconWhite(buttonImage, 0); + mRedirectButtonTextView = (TextView) button.findViewById(R.id.config_enable_button_text); + + updateTextViews(); + + setDialogTitle(mScriptResolver.getName()); + hideNegativeButton(); + onResolverStateUpdated(mScriptResolver); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + /** + * Initialize + */ + @Override + public void onResume() { + super.onResume(); + + if (mScriptResolver != null) { + updateTextViews(); + } + } + + private void updateTextViews() { + if (PluginUtils.isPluginUpToDate(mScriptResolver.getId())) { + mRedirectButtonTextView.setText(mScriptResolver.isEnabled() + ? getString(R.string.resolver_config_redirect_button_text_log_out_of) + : getString(R.string.resolver_config_redirect_button_text_log_into)); + mWarningTextView.setVisibility(View.GONE); + } else { + mRedirectButtonTextView.setText( + getString(R.string.resolver_config_redirect_button_text_download_plugin)); + mWarningTextView.setText(R.string.warn_closed_source_text); + mWarningTextView.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onConfigTestResult(Object component, int type, String message) { + if (mScriptResolver == component) { + if (type == AuthenticatorManager.CONFIG_TEST_RESULT_TYPE_SUCCESS) { + mRedirectButtonTextView.setText( + getString(R.string.resolver_config_redirect_button_text_log_out_of)); + } else { + mRedirectButtonTextView.setText( + getString(R.string.resolver_config_redirect_button_text_log_into)); + } + stopLoadingAnimation(); + } + } + + @Override + protected void onPositiveAction() { + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/SendLogConfigDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/SendLogConfigDialog.java new file mode 100644 index 000000000..c01754245 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/SendLogConfigDialog.java @@ -0,0 +1,129 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.acra.ACRA; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigEdittext; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +/** + * A {@link ConfigDialog} which shows a textfield to enter the reason why the user wants to send us + * a log. A click on "OK" will dispatch an email intent with the generated data. + */ +public class SendLogConfigDialog extends ConfigDialog { + + public final static String TAG = SendLogConfigDialog.class.getSimpleName(); + + public static String mLastEmail; + + public static String mLastUsermessage; + + private EditText mEmailEditText; + + private EditText mUserMessageEditText; + + public static class SendLogException extends Throwable { + + @Override + public String toString() { + return getDefaultString(); + } + + public static String getDefaultString() { + return SendLogException.class.getSimpleName() + + ": User manually requested to send a log"; + } + + } + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + TextView headerTextView = (TextView) addScrollingViewToFrame(R.layout.config_textview); + headerTextView.setText(R.string.preferences_app_sendlog_dialog_text); + mEmailEditText = (ConfigEdittext) addScrollingViewToFrame(R.layout.config_edittext); + mEmailEditText.setHint(R.string.preferences_app_sendlog_email); + mUserMessageEditText = + (ConfigEdittext) addScrollingViewToFrame(R.layout.config_edittext_multiplelines); + mUserMessageEditText.setHint(R.string.preferences_app_sendlog_issue); + + ViewUtils.showSoftKeyboard(mEmailEditText); + + setDialogTitle(getString(R.string.preferences_app_sendlog)); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + // Reset errors. + mUserMessageEditText.setError(null); + + // Store values at the time of the login attempt. + final String email = mEmailEditText.getText().toString(); + final String userMessage = mUserMessageEditText.getText().toString(); + + boolean cancel = false; + View focusView = null; + + // Check that the text field isn't empty + if (TextUtils.isEmpty(email)) { + mEmailEditText.setError(getString(R.string.error_field_required)); + focusView = mEmailEditText; + cancel = true; + } + + // Check that the text field isn't empty + if (TextUtils.isEmpty(userMessage)) { + mUserMessageEditText.setError(getString(R.string.error_field_required)); + focusView = mUserMessageEditText; + cancel = true; + } + + if (cancel) { + // There was an error; don't attempt to send the log and focus the first + // form field with an error. + focusView.requestFocus(); + } else { + mLastEmail = email; + mLastUsermessage = userMessage; + + ACRA.getErrorReporter().handleSilentException(new SendLogException()); + Toast.makeText(TomahawkApp.getContext(), R.string.crash_dialog_ok_toast, + Toast.LENGTH_LONG).show(); + dismiss(); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/WarnOldPluginDialog.java b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/WarnOldPluginDialog.java new file mode 100644 index 000000000..168f2dac3 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/dialogs/WarnOldPluginDialog.java @@ -0,0 +1,76 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.dialogs; + +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.widget.TextView; + +/** + * A {@link android.support.v4.app.DialogFragment} which is being shown to the user to warn him that + * he's using an old plugin version. + */ +public class WarnOldPluginDialog extends ConfigDialog { + + public final static String TAG = WarnOldPluginDialog.class.getSimpleName(); + + private ScriptResolver mScriptResolver; + + /** + * Called when this {@link android.support.v4.app.DialogFragment} is being created + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + String message = ""; + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.PREFERENCEID)) { + String id = getArguments().getString(TomahawkFragment.PREFERENCEID); + mScriptResolver = PipeLine.get().getResolver(id); + } + if (getArguments().containsKey(TomahawkFragment.MESSAGE)) { + message = getArguments().getString(TomahawkFragment.MESSAGE); + } + } + + TextView textview = (TextView) addScrollingViewToFrame(R.layout.config_textview); + textview.setText(message); + setDialogTitle(getString(android.R.string.dialog_alert_title)); + onResolverStateUpdated(mScriptResolver); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(getDialogView()); + return builder.create(); + } + + @Override + protected void onPositiveAction() { + ResolverRedirectConfigDialog dialog = new ResolverRedirectConfigDialog(); + Bundle args = new Bundle(); + args.putString(TomahawkFragment.PREFERENCEID, mScriptResolver.getId()); + dialog.setArguments(args); + dialog.show(getFragmentManager(), null); + dismiss(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/AlbumsFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/AlbumsFragment.java new file mode 100644 index 000000000..b4d7fe5c8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/AlbumsFragment.java @@ -0,0 +1,365 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.AlwaysCallback; +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.jdeferred.android.AndroidDeferredManager; +import org.jdeferred.multiple.MultipleResults; +import org.jdeferred.multiple.OneReject; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.AlphaComparator; +import org.tomahawk.libtomahawk.collection.ArtistAlphaComparator; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionCursor; +import org.tomahawk.libtomahawk.collection.HatchetCollection; +import org.tomahawk.libtomahawk.collection.LastModifiedComparator; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; + +import android.os.Bundle; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link TomahawkFragment} which shows a set of {@link Album}s inside its {@link + * se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ +public class AlbumsFragment extends TomahawkFragment { + + private static final String TAG = AlbumsFragment.class.getSimpleName(); + + public static final String COLLECTION_ALBUMS_SPINNER_POSITION + = "org.tomahawk.tomahawk_android.collection_albums_spinner_position_"; + + @Override + public void onResume() { + super.onResume(); + + mHideRemoveButton = true; + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + updateAdapter(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * + * @param view the clicked view + * @param item the Object which corresponds to the click + */ + @Override + public void onItemClick(View view, final Object item, Segment segment) { + if (getMediaController() == null) { + Log.e(TAG, "onItemClick failed because getMediaController() is null"); + return; + } + if (item instanceof PlaylistEntry) { + final PlaylistEntry entry = (PlaylistEntry) item; + if (entry.getQuery().isPlayable()) { + if (getPlaybackManager().getCurrentEntry() == entry) { + // if the user clicked on an already playing track + int playState = getMediaController().getPlaybackState().getState(); + if (playState == PlaybackStateCompat.STATE_PLAYING) { + getMediaController().getTransportControls().pause(); + } else if (playState == PlaybackStateCompat.STATE_PAUSED) { + getMediaController().getTransportControls().play(); + } + } else { + if (!TomahawkApp.PLUGINNAME_HATCHET.equals(mCollection.getId())) { + mCollection.getArtistTracks(mArtist).done(new DoneCallback() { + @Override + public void onDone(Playlist topHits) { + getPlaybackManager().setPlaylist(topHits, entry); + getMediaController().getTransportControls().play(); + } + }); + } else { + HatchetCollection collection = (HatchetCollection) mCollection; + collection.getArtistTopHits(mArtist).done(new DoneCallback() { + @Override + public void onDone(Playlist topHits) { + getPlaybackManager().setPlaylist(topHits, entry); + getMediaController().getTransportControls().play(); + } + }); + } + } + } + } else if (item instanceof Album) { + Album album = (Album) item; + mCollection.getAlbumTracks(album).done(new DoneCallback() { + @Override + public void onDone(Playlist playlist) { + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.ALBUM, ((Album) item).getCacheKey()); + if (playlist != null) { + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + } else { + bundle.putString( + TomahawkFragment.COLLECTION_ID, TomahawkApp.PLUGINNAME_HATCHET); + } + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + PlaylistEntriesFragment.class, bundle); + } + }); + } + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + if (mArtist != null) { + if (!TomahawkApp.PLUGINNAME_HATCHET.equals(mCollection.getId())) { + final List segments = new ArrayList<>(); + List promises = new ArrayList<>(); + promises.add(mCollection.getArtistTracks(mArtist)); + promises.add(mCollection.getArtistAlbums(mArtist)); + AndroidDeferredManager deferredManager = new AndroidDeferredManager(); + deferredManager.when(promises.toArray(new Promise[promises.size()])).always( + new AlwaysCallback() { + @Override + public void onAlways(Promise.State state, MultipleResults resolved, + OneReject rejected) { + Playlist artistTracks = (Playlist) resolved.get(0).getResult(); + Segment segment = new Segment.Builder(artistTracks) + .headerLayout(R.layout.single_line_list_header) + .headerString(mCollection.getName() + " " + + TomahawkApp.getContext().getString( + R.string.tracks)) + .showNumeration(true, 1) + .hideArtistName(true) + .showDuration(true) + .build(); + segments.add(0, segment); + + CollectionCursor cursor = + (CollectionCursor) resolved.get(1).getResult(); + segment = new Segment.Builder(cursor) + .headerLayout(R.layout.single_line_list_header) + .headerString(mCollection.getName() + " " + + TomahawkApp.getContext().getString( + R.string.albums)) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + segments.add(segment); + fillAdapter(segments, mCollection); + } + }); + } else { + HatchetCollection collection = (HatchetCollection) mCollection; + final List segments = new ArrayList<>(); + List promises = new ArrayList<>(); + promises.add(collection.getArtistTopHits(mArtist)); + promises.add(collection.getArtistAlbums(mArtist)); + AndroidDeferredManager deferredManager = new AndroidDeferredManager(); + deferredManager.when(promises.toArray(new Promise[promises.size()])).always( + new AlwaysCallback() { + @Override + public void onAlways(Promise.State state, MultipleResults resolved, + OneReject rejected) { + Playlist artistTophits = (Playlist) resolved.get(0).getResult(); + Segment segment = new Segment.Builder(artistTophits) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.top_hits) + .showNumeration(true, 1) + .hideArtistName(true) + .showDuration(true) + .build(); + segments.add(0, segment); + + CollectionCursor cursor = + (CollectionCursor) resolved.get(1).getResult(); + List albumsAndEps = new ArrayList<>(); + /* Remove this for now since all "other releases" albums returned by + Hatchet are empty + List others = new ArrayList<>(); + */ + if (cursor != null) { + for (int i = 0; i < cursor.size(); i++) { + Album album = cursor.get(i); + if (album.getReleaseType() != null + && (Album.RELEASETYPE_ALBUM.equals( + album.getReleaseType()) + || Album.RELEASETYPE_EPS.equals( + album.getReleaseType()))) { + albumsAndEps.add(album); + } + /* Remove this for now since all "other releases" albums returned by + Hatchet are empty + else { + others.add(album); + } + */ + } + } + segment = new Segment.Builder(albumsAndEps) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.albums_and_eps) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + segments.add(segment); + /* Remove this for now since all "other releases" albums returned by + Hatchet are empty + segment = new Segment.Builder(others) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.other_releases) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + segments.add(segment); + */ + fillAdapter(segments); + } + }); + } + } else if (mAlbumArray != null) { + Segment.Builder builder = new Segment.Builder(mAlbumArray); + if (mContainerFragmentClass != null + && mContainerFragmentClass.equals(ChartsPagerFragment.class.getName())) { + builder.showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .showNumeration(true, 1); + } + Segment segment = builder.build(); + fillAdapter(segment); + } else if (mUser != null) { + String id = mCollection.getId(); + Segment segment = new Segment.Builder(sortLovedAlbums(mUser, mUser.getStarredAlbums())) + .headerLayout(R.layout.dropdown_header) + .headerStrings(constructDropdownItems()) + .spinner(constructDropdownListener(COLLECTION_ALBUMS_SPINNER_POSITION + id), + getDropdownPos(COLLECTION_ALBUMS_SPINNER_POSITION + id)) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + fillAdapter(segment); + } else { + mCollection.getAlbums(getSortMode()).done(new DoneCallback>() { + @Override + public void onDone(final CollectionCursor cursor) { + new Thread(new Runnable() { + @Override + public void run() { + String id = mCollection.getId(); + Segment segment = new Segment.Builder(cursor) + .headerLayout(R.layout.dropdown_header) + .headerStrings(constructDropdownItems()) + .spinner(constructDropdownListener( + COLLECTION_ALBUMS_SPINNER_POSITION + id), + getDropdownPos(COLLECTION_ALBUMS_SPINNER_POSITION + id)) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + fillAdapter(segment, mCollection); + } + }).start(); + } + }); + } + } + + private List constructDropdownItems() { + List dropDownItems = new ArrayList<>(); + if (!(mCollection instanceof ScriptResolverCollection)) { + dropDownItems.add(R.string.collection_dropdown_recently_added); + } + dropDownItems.add(R.string.collection_dropdown_alpha); + dropDownItems.add(R.string.collection_dropdown_alpha_artists); + return dropDownItems; + } + + private int getSortMode() { + String id = mCollection.getId(); + int pos = getDropdownPos(COLLECTION_ALBUMS_SPINNER_POSITION + id); + if (!(mCollection instanceof ScriptResolverCollection)) { + switch (pos) { + case 0: + return Collection.SORT_LAST_MODIFIED; + case 1: + return Collection.SORT_ALPHA; + case 2: + return Collection.SORT_ARTIST_ALPHA; + default: + return Collection.SORT_NOT; + } + } else { + switch (pos) { + case 0: + return Collection.SORT_ALPHA; + case 1: + return Collection.SORT_ARTIST_ALPHA; + default: + return Collection.SORT_NOT; + } + } + } + + private List sortLovedAlbums(User user, List albums) { + String id = mCollection.getId(); + switch (getDropdownPos(COLLECTION_ALBUMS_SPINNER_POSITION + id)) { + case 0: + Map timestamps = new HashMap<>(); + for (Album album : albums) { + timestamps.put(album, user.getRelationship(album).getDate().getTime()); + } + Collections.sort(albums, new LastModifiedComparator<>(timestamps)); + break; + case 1: + Collections.sort(albums, new AlphaComparator()); + break; + case 2: + Collections.sort(albums, new ArtistAlphaComparator()); + break; + } + return albums; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistPagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistPagerFragment.java new file mode 100644 index 000000000..59bb1f415 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistPagerFragment.java @@ -0,0 +1,150 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.views.FancyDropDown; + +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class ArtistPagerFragment extends PagerFragment { + + private static final String TAG = ArtistPagerFragment.class.getSimpleName(); + + private Artist mArtist; + + private int mInitialPage = -1; + + @SuppressWarnings("unused") + public void onEventMainThread(CollectionManager.UpdatedEvent event) { + if (event.mUpdatedItemIds != null + && event.mUpdatedItemIds.contains(mArtist.getCacheKey())) { + updatePager(); + } + } + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.ArtistPagerFragment}'s + * {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getActivity().setTitle(""); + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + mInitialPage = getArguments() + .getInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + if (getArguments().containsKey(TomahawkFragment.ARTIST)) { + mArtist = Artist.getByKey(getArguments().getString(TomahawkFragment.ARTIST)); + if (mArtist == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else { + String requestId = InfoSystem.get().resolve(mArtist, false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + } + + updatePager(); + } + + private void updatePager() { + showContentHeader(mArtist); + + setupPager(getFragmentInfoLists(), mInitialPage, null, 1); + CollectionManager.get().getAvailableCollections(mArtist) + .done(new DoneCallback>() { + @Override + public void onDone(final List result) { + int initialSelection = 0; + for (int i = 0; i < result.size(); i++) { + if (result.get(i).getId().equals( + getArguments().getString(TomahawkFragment.COLLECTION_ID))) { + initialSelection = i; + break; + } + } + getArguments().putString(TomahawkFragment.COLLECTION_ID, + result.get(initialSelection).getId()); + showFancyDropDown(initialSelection, mArtist.getPrettyName().toUpperCase(), + FancyDropDown.convertToDropDownItemInfo(result), + new FancyDropDown.DropDownListener() { + @Override + public void onDropDownItemSelected(int position) { + getArguments().putString(TomahawkFragment.COLLECTION_ID, + result.get(position).getId()); + fillAdapter(getFragmentInfoLists(), 0, 1); + } + + @Override + public void onCancel() { + } + }); + setupAnimations(); + } + }); + } + + private List getFragmentInfoLists() { + List fragmentInfoLists = new ArrayList<>(); + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = AlbumsFragment.class; + fragmentInfo.mTitle = getString(R.string.music); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle + .putString(TomahawkFragment.ARTIST, mArtist.getCacheKey()); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = BiographyFragment.class; + fragmentInfo.mTitle = getString(R.string.biography); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle + .putString(TomahawkFragment.ARTIST, mArtist.getCacheKey()); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + return fragmentInfoLists; + } + + @Override + protected void onInfoSystemResultsReported(InfoRequestData infoRequestData) { + if (mCorrespondingRequestIds.contains(infoRequestData.getRequestId())) { + showContentHeader(mArtist); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistsFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistsFragment.java new file mode 100644 index 000000000..31a34dc8a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ArtistsFragment.java @@ -0,0 +1,171 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionCursor; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link TomahawkFragment} which shows a set of {@link Artist}s inside its {@link + * se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ +public class ArtistsFragment extends TomahawkFragment { + + public static final String COLLECTION_ARTISTS_SPINNER_POSITION + = "org.tomahawk.tomahawk_android.collection_artists_spinner_position_"; + + @Override + public void onResume() { + super.onResume(); + + updateAdapter(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, final Object item, Segment segment) { + if (item instanceof Artist) { + Artist artist = (Artist) item; + mCollection.getArtistAlbums(artist).done(new DoneCallback>() { + @Override + public void onDone(CollectionCursor cursor) { + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.ARTIST, + ((Artist) item).getCacheKey()); + if (cursor != null && cursor.size() > 0) { + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + } else { + bundle.putString(TomahawkFragment.COLLECTION_ID, + TomahawkApp.PLUGINNAME_HATCHET); + } + if (cursor != null) { + cursor.close(); + } + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC_PAGER); + bundle.putLong(CONTAINER_FRAGMENT_ID, + IdGenerator.getSessionUniqueId()); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + ArtistPagerFragment.class, bundle); + } + }); + } + } + + /** + * Update this {@link TomahawkFragment}'s {@link org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter} + * content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + if (mArtistArray != null) { + Segment.Builder builder = new Segment.Builder(mArtistArray); + if (mContainerFragmentClass != null + && mContainerFragmentClass.equals(ChartsPagerFragment.class.getName())) { + builder.showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .showNumeration(true, 1); + } + Segment segment = builder.build(); + fillAdapter(segment); + } else { + mCollection.getArtists(getSortMode()) + .done(new DoneCallback>() { + @Override + public void onDone(final CollectionCursor cursor) { + new Thread(new Runnable() { + @Override + public void run() { + String id = mCollection.getId(); + Segment segment = new Segment.Builder(cursor) + .headerLayout(R.layout.dropdown_header) + .headerStrings(constructDropdownItems()) + .spinner(constructDropdownListener( + COLLECTION_ARTISTS_SPINNER_POSITION + id), + getDropdownPos( + COLLECTION_ARTISTS_SPINNER_POSITION + + id)) + .showAsGrid(R.integer.grid_column_count, + R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + fillAdapter(segment); + } + }).start(); + } + }); + } + } + + private List constructDropdownItems() { + List dropDownItems = new ArrayList<>(); + if (!(mCollection instanceof ScriptResolverCollection)) { + dropDownItems.add(R.string.collection_dropdown_recently_added); + } + dropDownItems.add(R.string.collection_dropdown_alpha); + return dropDownItems; + } + + private int getSortMode() { + String id = mCollection.getId(); + int pos = getDropdownPos(COLLECTION_ARTISTS_SPINNER_POSITION + id); + if (!(mCollection instanceof ScriptResolverCollection)) { + switch (pos) { + case 0: + return Collection.SORT_LAST_MODIFIED; + case 1: + return Collection.SORT_ALPHA; + default: + return Collection.SORT_NOT; + } + } else { + switch (pos) { + case 0: + return Collection.SORT_ALPHA; + default: + return Collection.SORT_NOT; + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/BiographyFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/BiographyFragment.java new file mode 100644 index 000000000..b1f13bdea --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/BiographyFragment.java @@ -0,0 +1,65 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.tomahawk_android.adapters.Segment; + +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class BiographyFragment extends TomahawkFragment { + + @Override + public void onResume() { + super.onResume(); + + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + updateAdapter(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + } + + /** + * Update this {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment}'s {@link + * org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + if (mArtist != null) { + List bioText = new ArrayList<>(); + bioText.add(mArtist.getBio()); + fillAdapter(new Segment.Builder(bioText).build()); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsPagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsPagerFragment.java new file mode 100644 index 000000000..348a0357f --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsPagerFragment.java @@ -0,0 +1,184 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.jdeferred.android.AndroidDeferredManager; +import org.jdeferred.multiple.MultipleResults; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsManager; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsProvider; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsResult; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; + +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class ChartsPagerFragment extends PagerFragment { + + public static final String CHARTSPROVIDER_ID = "chartsprovider_id"; + + public static final String CHARTSPROVIDER_COUNTRYCODE = "chartsprovider_countrycode"; + + private static final String CHARTS_PLAYLIST_SUFFX = "_charts_"; + + private ScriptChartsProvider mChartsProvider; + + /** + * Called, when this {@link ChartsPagerFragment}'s {@link View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final String countryCode; + if (getArguments().containsKey(CHARTSPROVIDER_ID)) { + String chartsProviderId = getArguments().getString(CHARTSPROVIDER_ID); + mChartsProvider = ScriptChartsManager.get().getScriptChartsProvider(chartsProviderId); + if (mChartsProvider == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } + } else { + throw new RuntimeException("No CHARTSPROVIDER_ID provided to ChartsPagerFragment"); + } + if (getArguments().containsKey(CHARTSPROVIDER_COUNTRYCODE)) { + countryCode = getArguments().getString(CHARTSPROVIDER_COUNTRYCODE); + if (countryCode == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } + } else { + throw new RuntimeException( + "No CHARTSPROVIDER_COUNTRYCODE provided to ChartsPagerFragment"); + } + + showContentHeader(mChartsProvider.getScriptAccount().getIconBackgroundPath()); + + mChartsProvider.getTypes().done(new DoneCallback>>() { + @Override + public void onDone(List> types) { + List promises = new ArrayList<>(); + for (Pair type : types) { + promises.add(mChartsProvider.getCharts(countryCode, type.second)); + } + showCharts(types, promises); + } + }); + } + + private void showCharts(final List> types, List promises) { + AndroidDeferredManager deferredManager = new AndroidDeferredManager(); + deferredManager.when(promises.toArray(new Promise[promises.size()])).done( + new DoneCallback() { + @Override + public void onDone(MultipleResults multipleResults) { + List fragmentInfoLists = new ArrayList<>(); + for (int i = 0; i < multipleResults.size(); i++) { + Object object = multipleResults.get(i).getResult(); + if (object instanceof ScriptChartsResult) { + ScriptChartsResult chartsResult = (ScriptChartsResult) object; + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mTitle = types.get(i).first; + if (chartsResult.contentType == PipeLine.URL_TYPE_ARTIST) { + fragmentInfo.mClass = ArtistsFragment.class; + ArrayList artistKeys = parseArtistCharts(chartsResult); + fragmentInfo.mBundle.putStringArrayList( + TomahawkFragment.ARTISTARRAY, artistKeys); + } else if (chartsResult.contentType == PipeLine.URL_TYPE_ALBUM) { + fragmentInfo.mClass = AlbumsFragment.class; + ArrayList albumKeys = parseAlbumCharts(chartsResult); + fragmentInfo.mBundle.putStringArrayList( + TomahawkFragment.ALBUMARRAY, albumKeys); + } else if (chartsResult.contentType == PipeLine.URL_TYPE_TRACK) { + fragmentInfo.mClass = PlaylistEntriesFragment.class; + Playlist pl = + parseTrackCharts(chartsResult, types.get(i).second); + fragmentInfo.mBundle.putString( + TomahawkFragment.PLAYLIST, pl.getCacheKey()); + } + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + } + } + setupPager(fragmentInfoLists, 0, null, 2); + } + }); + } + + private Playlist parseTrackCharts(ScriptChartsResult chartsResult, String type) { + List queries = new ArrayList<>(); + for (JsonObject rawTrack : chartsResult.results) { + JsonElement artist = rawTrack.get("artist"); + JsonElement album = rawTrack.get("album"); + JsonElement track = rawTrack.get("track"); + if (artist != null && album != null && track != null) { + String artistName = artist.getAsString(); + String albumName = album.getAsString(); + String trackName = track.getAsString(); + queries.add(Query.get(trackName, albumName, artistName, false)); + } + } + String id = mChartsProvider.getScriptAccount().getName() + CHARTS_PLAYLIST_SUFFX + type; + String name = mChartsProvider.getScriptAccount().getName() + "_" + type; + Playlist pl = Playlist.fromQueryList(id, name, null, queries); + pl.setFilled(true); + return pl; + } + + private ArrayList parseArtistCharts(ScriptChartsResult chartsResult) { + ArrayList artistKeys = new ArrayList<>(); + for (JsonObject rawArtist : chartsResult.results) { + JsonElement artist = rawArtist.get("artist"); + if (artist != null) { + String artistName = artist.getAsString(); + artistKeys.add(Artist.get(artistName).getCacheKey()); + } + } + return artistKeys; + } + + private ArrayList parseAlbumCharts(ScriptChartsResult chartsResult) { + ArrayList albumKeys = new ArrayList<>(); + for (JsonObject rawAlbum : chartsResult.results) { + JsonElement artist = rawAlbum.get("artist"); + JsonElement album = rawAlbum.get("album"); + if (artist != null && album != null) { + String artistName = artist.getAsString(); + Artist a = Artist.get(artistName); + String albumName = album.getAsString(); + albumKeys.add(Album.get(albumName, a).getCacheKey()); + } + } + return albumKeys; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsSelectorFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsSelectorFragment.java new file mode 100644 index 000000000..eb2d8e487 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ChartsSelectorFragment.java @@ -0,0 +1,359 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.jdeferred.android.AndroidDeferredManager; +import org.jdeferred.multiple.MultipleResults; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsCountryCodes; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsManager; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsProvider; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverMetaData; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.CountryCodeSpinnerAdapter; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.Selector; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.v4.app.Fragment; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.greenrobot.event.EventBus; + +public class ChartsSelectorFragment extends Fragment { + + private MenuItem mCountryCodePicker; + + private List mFragmentInfos = new ArrayList<>(); + + private FragmentInfo mSelectedFragmentInfo; + + @SuppressWarnings("unused") + public void onEventMainThread(ScriptChartsManager.ProviderAddedEvent event) { + setupView(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.selectorfragment_layout, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getActivity().setTitle(R.string.drawer_title_charts); + + setupView(); + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + + super.onStop(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + mCountryCodePicker = menu.findItem(R.id.action_country_code_picker); + mCountryCodePicker.setVisible(true); + + if (mSelectedFragmentInfo != null) { + String chartsProviderId = + mSelectedFragmentInfo.mBundle.getString(ChartsPagerFragment.CHARTSPROVIDER_ID); + ScriptChartsProvider provider = + ScriptChartsManager.get().getScriptChartsProvider(chartsProviderId); + populateCountryCodeSpinner(provider, true); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + private void setupView() { + if (ScriptChartsManager.get().getAllScriptChartsProvider().size() > 0) { + final List providers = new ArrayList<>(); + for (ScriptChartsProvider provider : + ScriptChartsManager.get().getAllScriptChartsProvider().values()) { + providers.add(provider); + } + Collections.sort(providers, new Comparator() { + @Override + public int compare(ScriptChartsProvider lhs, ScriptChartsProvider rhs) { + return lhs.getScriptAccount().getName() + .compareTo(rhs.getScriptAccount().getName()); + } + }); + List promises = new ArrayList<>(); + for (ScriptChartsProvider provider : providers) { + promises.add(provider.getCountryCodes()); + } + new AndroidDeferredManager().when(promises.toArray(new Promise[promises.size()])).done( + new DoneCallback() { + @Override + public void onDone(MultipleResults multipleResults) { + mFragmentInfos.clear(); + for (int i = 0; i < multipleResults.size(); i++) { + ScriptChartsCountryCodes result = (ScriptChartsCountryCodes) + multipleResults.get(i).getResult(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = ChartsPagerFragment.class; + ScriptResolverMetaData metaData = + providers.get(i).getScriptAccount().getMetaData(); + fragmentInfo.mTitle = metaData.name; + fragmentInfo.mBundle = new Bundle(); + fragmentInfo.mBundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_CHARTS); + fragmentInfo.mBundle.putString( + ChartsPagerFragment.CHARTSPROVIDER_ID, + providers.get(i).getScriptAccount().getName()); + + String countryCode = getStoredCountryCode(providers.get(i)); + if (countryCode == null) { + countryCode = result.defaultCode; + } + fragmentInfo.mBundle.putString( + ChartsPagerFragment.CHARTSPROVIDER_COUNTRYCODE, + countryCode); + mFragmentInfos.add(fragmentInfo); + } + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + setupSelector(); + if (getView() != null) { + getView().findViewById(R.id.circularprogressview_selector) + .setVisibility(View.GONE); + } + } + }); + } + }); + } + } + + protected void setupSelector() { + if (getView() != null) { + FragmentInfo defaultFragmentInfo = mFragmentInfos.get(0); + String lastDisplayedProviderId = getLastProviderId(); + if (lastDisplayedProviderId != null) { + for (FragmentInfo info : mFragmentInfos) { + if (lastDisplayedProviderId.equals( + info.mBundle.getString(ChartsPagerFragment.CHARTSPROVIDER_ID))) { + defaultFragmentInfo = info; + break; + } + } + } + showSelectedFragment(defaultFragmentInfo); + + final View selectorHeader = getView().findViewById(R.id.selectorHeader); + selectorHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setSelectorHeaderMode(true); + + Selector selector = (Selector) getView().findViewById(R.id.selector); + Selector.SelectorListener selectorListener = new Selector.SelectorListener() { + @Override + public void onSelectorItemSelected(int position) { + setSelectorHeaderMode(false); + + showSelectedFragment(mFragmentInfos.get(position)); + } + + @Override + public void onCancel() { + setSelectorHeaderMode(false); + } + }; + selector.setup(mFragmentInfos, selectorListener, + getActivity().findViewById(R.id.sliding_layout), null); + selector.showSelectorList(); + } + }); + } + } + + private void setSelectorHeaderMode(boolean selectorShown) { + if (getView() != null) { + ImageView arrowTop = (ImageView) getView().findViewById(R.id.arrow_top_header); + ImageView arrowBottom = (ImageView) getView().findViewById(R.id.arrow_bottom_header); + View selectorHeader = getView().findViewById(R.id.selectorHeader); + TextView textViewHeader = (TextView) getView().findViewById(R.id.textview_header); + + selectorHeader.setClickable(!selectorShown); + arrowTop.setVisibility(selectorShown ? View.GONE : View.VISIBLE); + arrowBottom.setVisibility(selectorShown ? View.GONE : View.VISIBLE); + textViewHeader.setVisibility(selectorShown ? View.VISIBLE : View.GONE); + } + } + + private void showSelectedFragment(FragmentInfo info) { + if (getView() != null) { + ImageView imageView = (ImageView) getView().findViewById(R.id.imageview_header); + TextView textView = (TextView) getView().findViewById(R.id.textview_header); + + mSelectedFragmentInfo = info; + String chartsProviderId = info.mBundle.getString(ChartsPagerFragment.CHARTSPROVIDER_ID); + storeLastProviderId(chartsProviderId); + ScriptChartsProvider provider = + ScriptChartsManager.get().getScriptChartsProvider(chartsProviderId); + populateCountryCodeSpinner(provider, true); + provider.getScriptAccount().loadIconWhite(imageView, 0); + textView.setText(info.mTitle.toUpperCase()); + textView.setVisibility(View.GONE); + + FragmentUtils.replace((TomahawkMainActivity) getActivity(), info.mClass, + mSelectedFragmentInfo.mBundle, R.id.content_frame); + } + } + + private void populateCountryCodeSpinner(final ScriptChartsProvider provider, + final boolean isInitial) { + if (mCountryCodePicker != null && provider != null) { + provider.getCountryCodes().done(new DoneCallback() { + @Override + public void onDone(final ScriptChartsCountryCodes result) { + final ArrayList countryCodes = new ArrayList<>(); + final ArrayList displayedCountryCodes = new ArrayList<>(); + final ArrayList displayedCountryCodeNames = new ArrayList<>(); + for (Pair countryCode : result.codes) { + countryCodes.add(countryCode.second); + displayedCountryCodes.add(countryCode.second.toUpperCase()); + displayedCountryCodeNames.add(countryCode.first); + } + int initialPosition = -1; + if (isInitial) { + // Must be the first call of this method. So we should set the initially + // stored (or default) selection of the spinner + String storedCountryCode = + ChartsSelectorFragment.this.getStoredCountryCode(provider); + if (storedCountryCode == null) { + storedCountryCode = result.defaultCode; + } + for (int i = 0; i < countryCodes.size(); i++) { + if (countryCodes.get(i).equalsIgnoreCase(storedCountryCode)) { + initialPosition = i; + } + } + } + Spinner mCountryCodePickerSpinner = (Spinner) mCountryCodePicker + .getActionView(); + CountryCodeSpinnerAdapter adapter = + new CountryCodeSpinnerAdapter(TomahawkApp.getContext(), + R.layout.spinner_textview_country_code, displayedCountryCodes, + displayedCountryCodeNames); + adapter.setDropDownViewResource( + R.layout.spinner_dropdown_textview_country_code); + mCountryCodePickerSpinner.setAdapter(adapter); + mCountryCodePickerSpinner + .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, + int position, long id) { + String selectedCountryCode = countryCodes.get(position); + String storedCountryCode = ChartsSelectorFragment.this + .getStoredCountryCode(provider); + if (storedCountryCode == null) { + storedCountryCode = result.defaultCode; + } + if (!storedCountryCode.equals(selectedCountryCode)) { + storeCountryCode(provider, selectedCountryCode); + mSelectedFragmentInfo.mBundle.putString( + ChartsPagerFragment.CHARTSPROVIDER_COUNTRYCODE, + selectedCountryCode); + // Refresh the currently shown ChartsPagerFragment + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + mSelectedFragmentInfo.mClass, + mSelectedFragmentInfo.mBundle, R.id.content_frame); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + if (initialPosition >= 0) { + mCountryCodePickerSpinner.setSelection(initialPosition); + } + } + }); + } + } + + private String getCountryCodeStorageKey(ScriptChartsProvider provider) { + return PreferenceUtils.CHARTS_COUNTRY_CODE + provider.getScriptAccount().getName(); + } + + private void storeCountryCode(ScriptChartsProvider provider, String countryCode) { + PreferenceUtils.edit() + .putString(getCountryCodeStorageKey(provider), countryCode).commit(); + } + + private String getStoredCountryCode(ScriptChartsProvider provider) { + return PreferenceUtils.getString(getCountryCodeStorageKey(provider)); + } + + private void storeLastProviderId(String lastProviderId) { + PreferenceUtils.edit() + .putString(PreferenceUtils.LAST_DISPLAYED_PROVIDER_ID, lastProviderId).commit(); + } + + private String getLastProviderId() { + return PreferenceUtils.getString(PreferenceUtils.LAST_DISPLAYED_PROVIDER_ID); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/CollectionPagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/CollectionPagerFragment.java new file mode 100644 index 000000000..fee912826 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/CollectionPagerFragment.java @@ -0,0 +1,94 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.DbCollection; +import org.tomahawk.libtomahawk.collection.UserCollection; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; + +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class CollectionPagerFragment extends PagerFragment { + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.CollectionPagerFragment}'s + * {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (getArguments().containsKey(TomahawkFragment.COLLECTION_ID)) { + String collectionId = getArguments().getString(TomahawkFragment.COLLECTION_ID); + Collection collection = CollectionManager.get().getCollection(collectionId); + if (collection == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } + getActivity().setTitle(collection.getName()); + if (collection instanceof UserCollection) { + showContentHeader(R.drawable.collection_header); + } else if (collection instanceof DbCollection) { + showContentHeader(((DbCollection) collection).getIconBackgroundPath()); + } + } else { + throw new RuntimeException("No collection-id provided to CollectionPagerFragment"); + } + + int initialPage = -1; + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + initialPage = getArguments().getInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + } + + List fragmentInfoLists = new ArrayList<>(); + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = ArtistsFragment.class; + fragmentInfo.mTitle = getString(R.string.artists); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = AlbumsFragment.class; + fragmentInfo.mTitle = getString(R.string.albums); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PlaylistEntriesFragment.class; + fragmentInfo.mTitle = getString(R.string.tracks); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + setupPager(fragmentInfoLists, initialPage, null, 2); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContentHeaderFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContentHeaderFragment.java new file mode 100644 index 000000000..a2921301e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContentHeaderFragment.java @@ -0,0 +1,755 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.PropertyValuesHolder; +import com.nineoldandroids.animation.ValueAnimator; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.adapters.ViewHolder; +import org.tomahawk.tomahawk_android.listeners.OnSizeChangedListener; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.views.FancyDropDown; +import org.tomahawk.tomahawk_android.views.PageIndicator; + +import android.content.res.Resources; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.v4.app.Fragment; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.util.Pair; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import de.greenrobot.event.EventBus; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class ContentHeaderFragment extends Fragment { + + private static final String TAG = ContentHeaderFragment.class.getSimpleName(); + + public static final String COLLECTION_ID = "collection_id"; + + public static final String CONTENT_HEADER_MODE = "content_header_mode"; + + public static final String CONTAINER_FRAGMENT_ID = "container_fragment_id"; + + public static final String CONTAINER_FRAGMENT_PAGE = "container_fragment_page"; + + public static final int MODE_HEADER_DYNAMIC = 0; + + public static final int MODE_HEADER_DYNAMIC_PAGER = 1; + + public static final int MODE_HEADER_STATIC = 2; + + public static final int MODE_HEADER_STATIC_USER = 3; + + public static final int MODE_ACTIONBAR_FILLED = 4; + + public static final int MODE_HEADER_STATIC_SMALL = 5; + + public static final int MODE_HEADER_PLAYBACK = 6; + + public static final int MODE_HEADER_STATIC_CHARTS = 7; + + public static final int MODE_HEADER_NONE = 8; + + public static final int ANIM_BUTTON_ID = 0; + + public static final int ANIM_FANCYDROPDOWN_ID = 1; + + public static final int ANIM_IMAGEVIEW_ID = 2; + + public static final int ANIM_ALBUMART_ID = 3; + + public static final int ANIM_PAGEINDICATOR_ID = 4; + + public static class AnimateEvent { + + public int mPlayTime; + + public long mContainerFragmentId; + + public int mContainerFragmentPage; + } + + public static class PerformSyncEvent { + + public int mContainerFragmentPage; + + public long mContainerFragmentId; + + public int mFirstVisiblePosition; + + public int mTop; + } + + public static class RequestSyncEvent { + + public long mContainerFragmentId; + + public int mPerformerFragmentPage; + + public int mReceiverFragmentPage; + } + + public static class MediaControllerConnectedEvent { + + } + + private final SparseArray mAnimators = new SparseArray<>(); + + protected boolean mShowFakeFollowing = false; + + protected boolean mShowFakeNotFollowing = false; + + protected int mHeaderScrollableHeight = 0; + + protected int mHeaderNonscrollableHeight = 0; + + private int mCurrentMode = -1; + + protected View.OnClickListener mFollowButtonListener; + + private int mLastPlayTime; + + protected long mContainerFragmentId = -1; + + protected int mContainerFragmentPage = -1; + + protected Collection mCollection; + + protected boolean mHideRemoveButton; + + private PlaybackManager mPlaybackManager; + + @SuppressWarnings("unused") + public void onEventMainThread(MediaControllerConnectedEvent event) { + if (getMediaController() != null) { + String playbackManagerId = getMediaController().getExtras() + .getString(PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + mPlaybackManager = PlaybackManager.getByKey(playbackManagerId); + onMediaControllerConnected(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Resources res = getResources(); + if (getArguments() != null) { + mCurrentMode = getArguments().getInt(TomahawkFragment.CONTENT_HEADER_MODE, -1); + + switch (mCurrentMode) { + case MODE_HEADER_DYNAMIC: + mHeaderScrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_scrollable); + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable); + break; + case MODE_HEADER_DYNAMIC_PAGER: + mHeaderScrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_scrollable); + mHeaderNonscrollableHeight = + res.getDimensionPixelSize(R.dimen.header_clear_space_nonscrollable) + + res.getDimensionPixelSize(R.dimen.pager_indicator_height); + break; + case MODE_HEADER_STATIC: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable_static); + break; + case MODE_HEADER_STATIC_USER: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable_static_user); + break; + case MODE_HEADER_STATIC_SMALL: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material) + + res.getDimensionPixelSize(R.dimen.pager_indicator_height); + break; + case MODE_HEADER_STATIC_CHARTS: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable); + break; + case MODE_ACTIONBAR_FILLED: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material); + break; + case MODE_HEADER_PLAYBACK: + mHeaderNonscrollableHeight = res.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable_playback); + break; + case MODE_HEADER_NONE: + break; + default: + throw new RuntimeException("Missing or invalid ContentHeaderFragment mode"); + } + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_ID)) { + mContainerFragmentId = getArguments() + .getLong(TomahawkFragment.CONTAINER_FRAGMENT_ID); + } + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + mContainerFragmentPage = getArguments().getInt( + TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + } + + if (getMediaController() != null) { + String playbackManagerId = getMediaController().getExtras().getString( + PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + mPlaybackManager = PlaybackManager.getByKey(playbackManagerId); + } + } + + @Override + public void onResume() { + super.onResume(); + + if (mCurrentMode == MODE_ACTIONBAR_FILLED) { + ((TomahawkMainActivity) getActivity()).showFilledActionBar(); + } else if (mCurrentMode == MODE_HEADER_STATIC_SMALL) { + ((TomahawkMainActivity) getActivity()).showGradientActionBar(); + } + + if (getArguments() != null) { + if (getArguments().containsKey(COLLECTION_ID)) { + String collectionId = getArguments().getString(TomahawkFragment.COLLECTION_ID); + mCollection = CollectionManager.get().getCollection(collectionId); + } else { + mCollection = CollectionManager.get().getHatchetCollection(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + + ((TomahawkMainActivity) getActivity()).showGradientActionBar(); + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + + super.onStop(); + } + + protected void onMediaControllerConnected() { + } + + public MediaControllerCompat getMediaController() { + if (getActivity() != null) { + return getActivity().getSupportMediaController(); + } + Log.e(TAG, "getActivity() was null, couldn't get MediaController!"); + return null; + } + + public PlaybackManager getPlaybackManager() { + return mPlaybackManager; + } + + public boolean isDynamicHeader() { + return mHeaderScrollableHeight > 0; + } + + protected void showFancyDropDown(int initialSelection, String text, + List dropDownItemInfos, + FancyDropDown.DropDownListener dropDownListener) { + if (getView() == null) { + Log.e(TAG, "Couldn't setup FancyDropDown, because getView() is null!"); + return; + } + + FancyDropDown fancyDropDown = (FancyDropDown) getView().findViewById(R.id.fancydropdown); + if (fancyDropDown != null) { + fancyDropDown.setup(initialSelection, text.toUpperCase(), dropDownItemInfos, + dropDownListener); + } else { + Log.e(TAG, "Couldn't setup FancyDropDown, because there is no FancyDropDown in the view" + + " hierarchy"); + } + } + + /** + * Show a content header. A content header provides information about the current Collection + * object that the user has navigated to. Like an AlbumArt image with the {@link + * org.tomahawk.libtomahawk.collection.Album}s name, which is shown at the top of the listview, + * if the user browses to a particular {@link org.tomahawk.libtomahawk.collection.Album} in his + * {@link org.tomahawk.libtomahawk.collection.UserCollection}. + * + * @param item the Collection object's information to show in the header view + */ + protected void showContentHeader(final Object item) { + if (getView() == null) { + Log.e(TAG, "Couldn't setup content header, because getView() is null!"); + return; + } + + boolean isPagerFragment = this instanceof PagerFragment; + + //Inflate content header + int stubResId; + int inflatedId; + if (item instanceof User) { + stubResId = isPagerFragment ? R.id.content_header_user_pager_stub + : R.id.content_header_user_stub; + inflatedId = isPagerFragment ? R.id.content_header_user_pager + : R.id.content_header_user; + } else { + if (mHeaderScrollableHeight > 0) { + stubResId = isPagerFragment ? R.id.content_header_pager_stub + : R.id.content_header_stub; + inflatedId = isPagerFragment ? R.id.content_header_pager + : R.id.content_header; + } else { + stubResId = isPagerFragment ? R.id.content_header_static_pager_stub + : R.id.content_header_static_stub; + inflatedId = isPagerFragment ? R.id.content_header_static_pager + : R.id.content_header_static; + } + } + final View contentHeader = ViewUtils.ensureInflation(getView(), stubResId, inflatedId); + contentHeader.getLayoutParams().height = + mHeaderNonscrollableHeight + mHeaderScrollableHeight; + + //Now we fill the added views with data and inflate the correct imageview_grid depending on + //what we need + final int gridOneStubId = isPagerFragment ? R.id.imageview_grid_one_pager_stub + : R.id.imageview_grid_one_stub; + final int gridOneResId = isPagerFragment ? R.id.imageview_grid_one_pager + : R.id.imageview_grid_one; + if (item instanceof Integer) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + (Integer) item); + } else if (item instanceof String) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + (String) item); + } else if (item instanceof ColorDrawable) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + imageView.setImageDrawable((ColorDrawable) item); + } else if (mHeaderScrollableHeight > 0) { + View moreButton = getView().findViewById(R.id.more_button); + moreButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + FragmentUtils.showContextMenu((TomahawkMainActivity) getActivity(), item, + mCollection.getId(), false, mHideRemoveButton); + } + }); + + if (item instanceof Album) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView, + ((Album) item).getImage(), Image.getLargeImageSize(), false); + View stationButton = getView().findViewById(R.id.station_button); + stationButton.setVisibility(View.VISIBLE); + stationButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getMediaController() != null) { + List> artists = new ArrayList<>(); + artists.add(new Pair<>(((Album) item).getArtist(), "")); + StationPlaylist stationPlaylist = + StationPlaylist.get(artists, null, null); + if (stationPlaylist != getPlaybackManager().getPlaylist()) { + getPlaybackManager().setPlaylist(stationPlaylist); + getMediaController().getTransportControls().play(); + } + } + } + }); + } else if (item instanceof Artist) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView, + ((Artist) item).getImage(), Image.getLargeImageSize(), true); + View stationButton = getView().findViewById(R.id.station_button); + stationButton.setVisibility(View.VISIBLE); + stationButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getMediaController() != null) { + List> artists = new ArrayList<>(); + artists.add(new Pair<>((Artist) item, "")); + StationPlaylist stationPlaylist = + StationPlaylist.get(artists, null, null); + if (stationPlaylist != getPlaybackManager().getPlaylist()) { + getPlaybackManager().setPlaylist(stationPlaylist); + getMediaController().getTransportControls().play(); + } + } + } + }); + } else if (item instanceof Playlist) { + ViewHolder.fillView(getView(), (Playlist) item, + mHeaderNonscrollableHeight + mHeaderScrollableHeight, isPagerFragment); + View stationButton = getView().findViewById(R.id.station_button); + stationButton.setVisibility(View.VISIBLE); + stationButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getMediaController() != null) { + StationPlaylist stationPlaylist = StationPlaylist.get((Playlist) item); + if (stationPlaylist != getPlaybackManager().getPlaylist()) { + getPlaybackManager().setPlaylist(stationPlaylist); + getMediaController().getTransportControls().play(); + } + } + } + }); + } else if (item instanceof Query) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), imageView, + ((Query) item).getImage(), Image.getLargeImageSize(), + ((Query) item).hasArtistImage()); + } + } else { + if (item == null) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageView imageView = (ImageView) v.findViewById(R.id.imageview1); + imageView.setImageDrawable(new ColorDrawable( + getResources().getColor(R.color.userpage_default_background))); + } else if (item instanceof Image) { + View v = ViewUtils.ensureInflation(getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + mHeaderScrollableHeight; + ImageUtils.loadBlurredImageIntoImageView(TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), (Image) item, + Image.getSmallImageSize(), R.color.userpage_default_background); + } else if (item instanceof User) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(final User user) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + boolean showFollowing = false; + boolean showNotFollowing = false; + if (mShowFakeFollowing || mShowFakeNotFollowing) { + showFollowing = mShowFakeFollowing; + showNotFollowing = mShowFakeNotFollowing; + } else if (!user.isOffline()) { + showFollowing = item != user && user.getFollowings() != null + && user.getFollowings().containsKey(item); + showNotFollowing = item != user && (user.getFollowings() == null + || !user.getFollowings().containsKey(item)); + } + View v = ViewUtils.ensureInflation( + getView(), gridOneStubId, gridOneResId); + v.getLayoutParams().height = mHeaderNonscrollableHeight + + mHeaderScrollableHeight; + ImageUtils.loadBlurredImageIntoImageView( + TomahawkApp.getContext(), + (ImageView) v.findViewById(R.id.imageview1), + ((User) item).getImage(), Image.getSmallImageSize(), + R.color.userpage_default_background); + ImageUtils.loadUserImageIntoImageView(TomahawkApp.getContext(), + (ImageView) contentHeader.findViewById(R.id.userimageview1), + (User) item, Image.getSmallImageSize(), + (TextView) contentHeader.findViewById(R.id.usertextview1)); + TextView textView = + (TextView) contentHeader.findViewById(R.id.textview1); + textView.setText(((User) item).getName().toUpperCase()); + TextView followButton = + (TextView) contentHeader.findViewById(R.id.followbutton1); + if (showFollowing) { + followButton.setVisibility(View.VISIBLE); + followButton.setBackgroundResource( + R.drawable.selectable_background_button_green_filled); + followButton.setOnClickListener(mFollowButtonListener); + followButton.setText(TomahawkApp.getContext().getString( + R.string.content_header_following).toUpperCase()); + } else if (showNotFollowing) { + followButton.setVisibility(View.VISIBLE); + followButton.setBackgroundResource( + R.drawable.selectable_background_button_green); + followButton.setOnClickListener(mFollowButtonListener); + followButton.setText(TomahawkApp.getContext().getString( + R.string.content_header_follow).toUpperCase()); + } else { + followButton.setVisibility(View.GONE); + } + } + }); + } + }); + } + } + } + + /** + * Add a non-scrollable spacer to the top of the given view + */ + protected void setupNonScrollableSpacer(View view) { + //Add a non-scrollable spacer to the top of the given view + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams(); + params.topMargin = mHeaderNonscrollableHeight; + } + + protected void setupScrollableSpacer(TomahawkListAdapter adapter, + StickyListHeadersListView listView, View headerSpacerForwardView) { + if (adapter != null) { + adapter.setShowContentHeaderSpacer(mHeaderScrollableHeight, listView, + headerSpacerForwardView); + } else { + Log.d(TAG, "setupScrollableSpacer - Can't call setShowContentHeaderSpacer," + + " Adapter is null"); + } + } + + protected void addAnimator(int id, ValueAnimator animator) { + mAnimators.put(id, animator); + } + + protected void setupAnimations() { + if (getView() == null) { + Log.e(TAG, "Couldn't setup animations, because getView() is null!"); + return; + } + + mAnimators.clear(); + if (isDynamicHeader()) { + boolean isPagerFragment = this instanceof PagerFragment; + View header = getView().findViewById(isPagerFragment ? R.id.content_header_pager + : R.id.content_header); + if (header == null) { + header = getView().findViewById(isPagerFragment ? R.id.content_header_user_pager + : R.id.content_header_user); + } + setupFancyDropDownAnimation(header); + setupButtonAnimation(header); + setupPageIndicatorAnimation(header); + + View headerImage = getView() + .findViewById(isPagerFragment ? R.id.imageview_grid_one_pager + : R.id.imageview_grid_one); + if (headerImage == null || headerImage.getVisibility() == View.GONE) { + headerImage = getView() + .findViewById(isPagerFragment ? R.id.imageview_grid_two_pager + : R.id.imageview_grid_two); + } + if (headerImage == null || headerImage.getVisibility() == View.GONE) { + headerImage = getView() + .findViewById(isPagerFragment ? R.id.imageview_grid_three_pager + : R.id.imageview_grid_three); + } + + setupImageViewAnimation(headerImage); + } + } + + protected void refreshAnimations() { + animate(mLastPlayTime); + } + + private void setupFancyDropDownAnimation(final View view) { + if (view != null) { + final FancyDropDown fancyDropDown = + (FancyDropDown) view.findViewById(R.id.fancydropdown); + if (fancyDropDown != null) { + final Runnable r = new Runnable() { + @Override + public void run() { + // get resources first + Resources resources = TomahawkApp.getContext().getResources(); + int dropDownHeight = resources.getDimensionPixelSize( + R.dimen.show_context_menu_icon_height); + int actionBarHeight = resources.getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material); + int smallPadding = resources.getDimensionPixelSize( + R.dimen.padding_small); + int superLargePadding = resources.getDimensionPixelSize( + R.dimen.padding_superlarge); + + // now calculate the animation goal and instantiate the animation + int initialX = view.getWidth() / 2 - fancyDropDown.getWidth() / 2; + int initialY = view.getHeight() / 2 - dropDownHeight / 2; + PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", initialX, + superLargePadding); + PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", initialY, + actionBarHeight + smallPadding); + ValueAnimator animator = ObjectAnimator + .ofPropertyValuesHolder(fancyDropDown, pvhX, pvhY) + .setDuration(10000); + animator.setInterpolator(new LinearInterpolator()); + addAnimator(ANIM_FANCYDROPDOWN_ID, animator); + + refreshAnimations(); + } + }; + r.run(); + fancyDropDown.setOnSizeChangedListener(new OnSizeChangedListener() { + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + r.run(); + } + }); + } + } + } + + private void setupButtonAnimation(final View view) { + if (view != null) { + View moreButton = view.findViewById(R.id.button_panel); + if (moreButton != null) { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(moreButton) { + @Override + public void run() { + // get resources first + Resources resources = TomahawkApp.getContext().getResources(); + int buttonHeight = TomahawkApp.getContext().getResources() + .getDimensionPixelSize( + R.dimen.show_context_menu_icon_height); + int largePadding = TomahawkApp.getContext().getResources() + .getDimensionPixelSize(R.dimen.padding_large); + int smallPadding = resources + .getDimensionPixelSize(R.dimen.padding_small); + int actionBarHeight = resources.getDimensionPixelSize( + R.dimen.abc_action_bar_default_height_material); + View pageIndicator = view.findViewById(R.id.page_indicator); + int pageIndicatorHeight = 0; + if (pageIndicator != null + && pageIndicator.getVisibility() == View.VISIBLE) { + pageIndicatorHeight = TomahawkApp.getContext().getResources() + .getDimensionPixelSize(R.dimen.pager_indicator_height); + } + + // now calculate the animation goal and instantiate the animation + int initialY = view.getHeight() - buttonHeight - largePadding + - pageIndicatorHeight; + ValueAnimator animator = ObjectAnimator.ofFloat(getLayedOutView(), "y", + initialY, actionBarHeight + smallPadding) + .setDuration(10000); + animator.setInterpolator(new LinearInterpolator()); + addAnimator(ANIM_BUTTON_ID, animator); + + refreshAnimations(); + } + }); + } + } + } + + private void setupImageViewAnimation(final View view) { + if (view != null) { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(view) { + @Override + public void run() { + // now calculate the animation goal and instantiate the animation + int initialY = 0; + ValueAnimator animator = ObjectAnimator.ofFloat(getLayedOutView(), "y", + initialY, view.getHeight() / -3) + .setDuration(10000); + animator.setInterpolator(new LinearInterpolator()); + addAnimator(ANIM_IMAGEVIEW_ID, animator); + + refreshAnimations(); + } + }); + } + } + + private void setupPageIndicatorAnimation(final View view) { + if (view != null) { + final PageIndicator indicatorView = + (PageIndicator) view.findViewById(R.id.page_indicator); + if (indicatorView != null) { + final Runnable r = new Runnable() { + @Override + public void run() { + // now calculate the animation goal and instantiate the animation + int initialY = view.getHeight() - indicatorView.getHeight(); + ValueAnimator animator = ObjectAnimator.ofFloat(indicatorView, "y", + initialY, + mHeaderNonscrollableHeight - indicatorView.getHeight()) + .setDuration(10000); + animator.setInterpolator(new LinearInterpolator()); + addAnimator(ANIM_PAGEINDICATOR_ID, animator); + + refreshAnimations(); + } + }; + r.run(); + indicatorView.setOnSizeChangedListener(new OnSizeChangedListener() { + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + r.run(); + } + }); + } + } + } + + public void animate(int position) { + mLastPlayTime = position; + for (int i = 0; i < mAnimators.size(); i++) { + mAnimators.valueAt(i).setCurrentPlayTime(position); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContextMenuFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContextMenuFragment.java new file mode 100644 index 000000000..a0a8a22f0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/ContextMenuFragment.java @@ -0,0 +1,776 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.collection.UserCollection; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.BlurTransformation; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.utils.ShareUtils; +import org.tomahawk.tomahawk_android.views.PlaybackPanel; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.util.Pair; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import de.greenrobot.event.EventBus; + +/** + * A {@link DialogFragment} which emulates the appearance and behaviour of the standard context menu + * dialog, so that it is fully customizable. + */ +public class ContextMenuFragment extends Fragment { + + private final static String TAG = ContextMenuFragment.class.getSimpleName(); + + private Album mAlbum; + + private Artist mArtist; + + private Playlist mPlaylist; + + private StationPlaylist mStationPlaylist; + + private PlaylistEntry mPlaylistEntry; + + private Query mQuery; + + private Collection mCollection; + + private boolean mFromPlaybackFragment; + + private boolean mHideRemoveButton; + + private final HashSet mCorrespondingRequestIds = new HashSet<>(); + + @SuppressWarnings("unused") + public void onEventMainThread(InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + setupAlbumArt(getView()); + setupContextMenuItems(getView()); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + unpackArgs(); + resolveItems(); + int layoutResId = mFromPlaybackFragment ? R.layout.context_menu_fragment_playback + : R.layout.context_menu_fragment; + return inflater.inflate(layoutResId, container, false); + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setupCloseButton(view); + setupContextMenuItems(view); + setupBlurredBackground(view); + + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + if (mFromPlaybackFragment) { + setupPlaybackTextViews(view, activity.getPlaybackPanel()); + activity.getPlaybackPanel().showButtons(); + activity.getPlaybackPanel().hideStationContainer(); + } else { + setupTextViews(view); + setupAlbumArt(view); + activity.hidePlaybackPanel(); + } + } + + @Override + public void onStop() { + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + if (mFromPlaybackFragment) { + activity.getPlaybackPanel().hideButtons(); + activity.getPlaybackPanel().showStationContainer(); + } else { + activity.showPlaybackPanel(false); + } + + EventBus.getDefault().unregister(this); + + super.onStop(); + } + + private void unpackArgs() { + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.HIDE_REMOVE_BUTTON)) { + mHideRemoveButton = getArguments() + .getBoolean(TomahawkFragment.HIDE_REMOVE_BUTTON); + } + if (getArguments().containsKey(TomahawkFragment.FROM_PLAYBACKFRAGMENT)) { + mFromPlaybackFragment = getArguments() + .getBoolean(TomahawkFragment.FROM_PLAYBACKFRAGMENT); + } + if (getArguments().containsKey(TomahawkFragment.TOMAHAWKLISTITEM_TYPE) + && getArguments().containsKey(TomahawkFragment.TOMAHAWKLISTITEM)) { + String type = getArguments().getString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE); + String key = getArguments().getString(TomahawkFragment.TOMAHAWKLISTITEM); + switch (type) { + case TomahawkFragment.ALBUM: + mAlbum = Album.getByKey(key); + break; + case TomahawkFragment.PLAYLIST: + mPlaylist = Playlist.getByKey(key); + break; + case TomahawkFragment.STATION: + mStationPlaylist = (StationPlaylist) Playlist.getByKey(key); + break; + case TomahawkFragment.ARTIST: + mArtist = Artist.getByKey(key); + break; + case TomahawkFragment.QUERY: + mQuery = Query.getByKey(key); + break; + case TomahawkFragment.SOCIALACTION: + SocialAction socialAction = SocialAction.getByKey(key); + Object targetObject = socialAction.getTargetObject(); + if (targetObject instanceof Artist) { + mArtist = (Artist) targetObject; + } else if (targetObject instanceof Album) { + mAlbum = (Album) targetObject; + } else if (targetObject instanceof Query) { + mQuery = (Query) targetObject; + } else if (targetObject instanceof Playlist) { + mPlaylist = (Playlist) targetObject; + } + break; + case TomahawkFragment.PLAYLISTENTRY: + mPlaylistEntry = PlaylistEntry.getByKey(key); + break; + } + } + if (getArguments().containsKey(TomahawkFragment.COLLECTION_ID)) { + String collectionId = getArguments().getString(TomahawkFragment.COLLECTION_ID); + mCollection = CollectionManager.get().getCollection(collectionId); + } + } + } + + private void resolveItems() { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + if (mCollection != null && mAlbum != null) { + String requestId = InfoSystem.get().resolve(mAlbum); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + if (mPlaylist != null && mPlaylist.getUserId() != null + && !mPlaylist.getUserId().equals(result.getId())) { + String requestId = InfoSystem.get().resolve(mPlaylist); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + + } + }); + } + + private void setupBlurredBackground(final View view) { + final View rootView = getActivity().findViewById(R.id.sliding_layout); + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(rootView) { + @Override + public void run() { + Bitmap bm = Bitmap.createBitmap(rootView.getWidth(), + rootView.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bm); + rootView.draw(canvas); + bm = Bitmap.createScaledBitmap(bm, bm.getWidth() / 4, + bm.getHeight() / 4, true); + bm = new BlurTransformation(getContext(), 25).transform(bm); + + ImageView bgImageView = + (ImageView) view.findViewById(R.id.background); + bgImageView.setImageBitmap(bm); + } + }); + } + + private void setupCloseButton(View view) { + View closeButton = view.findViewById(R.id.close_button); + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + if (activity.getSlidingUpPanelLayout().getPanelState() + != SlidingUpPanelLayout.PanelState.HIDDEN) { + AnimationUtils.fade(activity.getPlaybackPanel(), + AnimationUtils.DURATION_CONTEXTMENU, true); + } + } + }); + TextView closeButtonText = (TextView) closeButton.findViewById(R.id.close_button_text); + closeButtonText.setText(getString(R.string.button_close).toUpperCase()); + } + + private void setupContextMenuItems(final View view) { + if (view == null) { + return; + } + + final TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + + // set up "Add to playlist" context menu item + if (mAlbum != null) { + mCollection.getAlbumTracks(mAlbum).done(new DoneCallback() { + @Override + public void onDone(Playlist result) { + List queries = null; + if (result != null && result.isFilled()) { + queries = new ArrayList<>(); + for (PlaylistEntry entry : result.getEntries()) { + queries.add(entry.getQuery()); + } + } + setupAddToPlaylistButton(view, queries); + } + }); + } else if (mPlaylist != null) { + List queries = null; + if (mPlaylist.isFilled()) { + queries = new ArrayList<>(); + for (PlaylistEntry entry : mPlaylist.getEntries()) { + queries.add(entry.getQuery()); + } + } + setupAddToPlaylistButton(view, queries); + } else if (mQuery != null || mPlaylistEntry != null) { + Query q = mQuery; + if (mPlaylistEntry != null) { + q = mPlaylistEntry.getQuery(); + } + ArrayList queries = new ArrayList<>(); + queries.add(q); + setupAddToPlaylistButton(view, queries); + } + + // set up "Create station" context menu item + if (mAlbum != null || mArtist != null || mPlaylist != null || mPlaylistEntry != null + || mQuery != null) { + setupCreateStationButton(view, mAlbum, mArtist, mPlaylist, mPlaylistEntry, mQuery); + } + + // set up "Add to collection" context menu item + if (mAlbum != null || mArtist != null) { + int drawableResId; + int stringResId; + UserCollection userCollection = CollectionManager.get().getUserCollection(); + if ((mAlbum != null && userCollection.isLoved(mAlbum)) + || (mArtist != null && userCollection.isLoved(mArtist))) { + drawableResId = R.drawable.ic_action_collection_underlined; + stringResId = R.string.context_menu_removefromcollection; + } else { + drawableResId = R.drawable.ic_action_collection; + stringResId = R.string.context_menu_addtocollection; + } + View v = ViewUtils.ensureInflation(view, R.id.context_menu_addtocollection_stub, + R.id.context_menu_addtocollection); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(drawableResId); + textView.setText(stringResId); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + if (mAlbum != null) { + CollectionManager.get().toggleLovedItem(mAlbum); + } else { + CollectionManager.get().toggleLovedItem(mArtist); + } + } + }); + } + + // set up "Add to favorites" context menu item + if (mQuery != null || mPlaylistEntry != null) { + final Query query = mQuery != null ? mQuery : mPlaylistEntry.getQuery(); + int drawableResId; + int stringResId; + if (DatabaseHelper.get().isItemLoved(query)) { + drawableResId = R.drawable.ic_action_favorites_underlined; + stringResId = R.string.context_menu_unlove; + } else { + drawableResId = R.drawable.ic_action_favorites; + stringResId = R.string.context_menu_love; + } + View v = ViewUtils.ensureInflation(view, R.id.context_menu_favorite_stub, + R.id.context_menu_favorite); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(drawableResId); + textView.setText(stringResId); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + CollectionManager.get().toggleLovedItem(query); + } + }); + } + + // set up "Share" context menu item + if (mStationPlaylist == null) { + View v = ViewUtils.ensureInflation( + view, R.id.context_menu_share_stub, R.id.context_menu_share); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(R.drawable.ic_action_share); + textView.setText(R.string.context_menu_share); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean error = false; + if (mAlbum != null) { + ShareUtils.sendShareIntent(activity, mAlbum); + } else if (mArtist != null) { + ShareUtils.sendShareIntent(activity, mArtist); + } else if (mQuery != null) { + ShareUtils.sendShareIntent(activity, mQuery); + } else if (mPlaylistEntry != null) { + ShareUtils.sendShareIntent(activity, mPlaylistEntry.getQuery()); + } else if (mPlaylist != null) { + if (mPlaylist.getHatchetId() == null) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(TomahawkApp.getContext(), + R.string.contest_menu_share_error, Toast.LENGTH_LONG) + .show(); + } + }); + error = true; + } else { + ShareUtils.sendShareIntent(activity, mPlaylist); + } + } + if (!error) { + getActivity().getSupportFragmentManager().popBackStack(); + } + } + }); + } + + // set up "Remove" context menu item + if (!mHideRemoveButton && (mPlaylist != null || mPlaylistEntry != null + || mStationPlaylist != null)) { + int stringResId; + if (mPlaylistEntry != null) { + stringResId = R.string.context_menu_removefromplaylist; + } else { + stringResId = R.string.context_menu_delete; + } + View v = ViewUtils.ensureInflation(view, R.id.context_menu_remove_stub, + R.id.context_menu_remove); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(R.drawable.ic_navigation_close); + textView.setText(stringResId); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + if (mStationPlaylist != null) { + DatabaseHelper.get().deleteStation(mStationPlaylist); + } else { + String localPlaylistId = mPlaylist != null ? mPlaylist.getId() + : mPlaylistEntry.getPlaylistId(); + if (mPlaylistEntry != null) { + CollectionManager.get().deletePlaylistEntry(localPlaylistId, + mPlaylistEntry.getId()); + } else { + CollectionManager.get().deletePlaylist(localPlaylistId); + } + } + } + }); + } + + // set up "Add to queue" context menu item + if (mAlbum != null || mQuery != null || mPlaylistEntry != null || mPlaylist != null) { + int drawableResId = R.drawable.ic_action_queue; + int stringResId = R.string.context_menu_add_to_queue; + View v = ViewUtils.ensureInflation(view, R.id.context_menu_addtoqueue_stub, + R.id.context_menu_addtoqueue); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(drawableResId); + textView.setText(stringResId); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + if (mAlbum != null) { + mCollection.getAlbumTracks(mAlbum).done(new DoneCallback() { + @Override + public void onDone(Playlist playlist) { + ArrayList queryKeys = new ArrayList<>(); + if (playlist != null) { + for (PlaylistEntry entry : playlist.getEntries()) { + queryKeys.add(entry.getQuery().getCacheKey()); + } + } + Bundle extras = new Bundle(); + extras.putStringArrayList(TomahawkFragment.QUERYARRAY, queryKeys); + getActivity().getSupportMediaController() + .getTransportControls().sendCustomAction( + PlaybackService.ACTION_ADD_QUERIES_TO_QUEUE, extras); + } + }); + } else if (mQuery != null) { + Bundle extras = new Bundle(); + extras.putString(TomahawkFragment.QUERY, mQuery.getCacheKey()); + getActivity().getSupportMediaController() + .getTransportControls().sendCustomAction( + PlaybackService.ACTION_ADD_QUERY_TO_QUEUE, extras); + } else if (mPlaylistEntry != null) { + Bundle extras = new Bundle(); + extras.putString(TomahawkFragment.QUERY, + mPlaylistEntry.getQuery().getCacheKey()); + getActivity().getSupportMediaController() + .getTransportControls().sendCustomAction( + PlaybackService.ACTION_ADD_QUERY_TO_QUEUE, extras); + } else if (mPlaylist != null) { + ArrayList queryKeys = new ArrayList<>(); + if (mPlaylist != null) { + for (PlaylistEntry entry : mPlaylist.getEntries()) { + queryKeys.add(entry.getQuery().getCacheKey()); + } + } + Bundle extras = new Bundle(); + extras.putStringArrayList(TomahawkFragment.QUERYARRAY, queryKeys); + getActivity().getSupportMediaController() + .getTransportControls().sendCustomAction( + PlaybackService.ACTION_ADD_QUERIES_TO_QUEUE, extras); + } + } + }); + } + } + + private void setupCreateStationButton(View view, final Album album, final Artist artist, + final Playlist playlist, final PlaylistEntry entry, final Query query) { + View v = ViewUtils.ensureInflation(view, R.id.context_menu_createstation_stub, + R.id.context_menu_createstation); + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(R.drawable.ic_action_station); + textView.setText(R.string.context_menu_create_station); + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + if (getActivity().getSupportMediaController() != null) { + String playbackManagerId = getActivity().getSupportMediaController().getExtras() + .getString(PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + PlaybackManager playbackManager = PlaybackManager.getByKey(playbackManagerId); + StationPlaylist stationPlaylist = null; + if (album != null) { + List> artists = new ArrayList<>(); + artists.add(new Pair<>(album.getArtist(), "")); + stationPlaylist = StationPlaylist.get(artists, null, null); + } else if (artist != null) { + List> artists = new ArrayList<>(); + artists.add(new Pair<>(artist, "")); + stationPlaylist = StationPlaylist.get(artists, null, null); + } else if (playlist != null) { + stationPlaylist = StationPlaylist.get(playlist); + } else if (entry != null || query != null) { + List> tracks = new ArrayList<>(); + if (query != null) { + tracks.add(new Pair<>(query.getBasicTrack(), "")); + } else { + tracks.add(new Pair<>(entry.getQuery().getBasicTrack(), "")); + } + stationPlaylist = StationPlaylist.get(null, tracks, null); + } + if (stationPlaylist != null + && stationPlaylist != playbackManager.getPlaylist()) { + playbackManager.setPlaylist(stationPlaylist); + getActivity().getSupportMediaController().getTransportControls().play(); + } + } + } + }); + } + + /** + * Initializes the "Add to playlist"-context-menu-button. + * + * @param view this Fragment's root View + * @param queries the List of {@link Query}s that should be added to one of the user's playlists + * once he taps the "add to playlist-context-menu-button. If this List is null + * the button will show up as disabled and greyed out. If this List is empty the + * button won't be displayed at all. + */ + private void setupAddToPlaylistButton(View view, final List queries) { + View v = ViewUtils.ensureInflation(view, R.id.context_menu_addtoplaylist_stub, + R.id.context_menu_addtoplaylist); + if (queries != null && queries.size() == 0) { + v.setVisibility(View.GONE); + } else { + TextView textView = (TextView) v.findViewById(R.id.textview); + ImageView imageView = (ImageView) v.findViewById(R.id.imageview); + imageView.setImageResource(R.drawable.ic_action_playlist); + textView.setText(R.string.context_menu_add_to_playlist); + if (queries != null) { + v.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + showAddToPlaylist((TomahawkMainActivity) getActivity(), queries); + } + }); + textView.setTextColor(getResources().getColor(R.color.primary_textcolor_inverted)); + ImageUtils.setTint(imageView.getDrawable(), R.color.primary_textcolor_inverted); + } else { + textView.setTextColor(getResources().getColor(R.color.disabled)); + ImageUtils.setTint(imageView.getDrawable(), R.color.disabled); + } + } + } + + private void showAddToPlaylist(final TomahawkMainActivity activity, final List queries) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + ArrayList queryKeys = new ArrayList<>(); + for (Query query : queries) { + queryKeys.add(query.getCacheKey()); + } + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + bundle.putStringArrayList(TomahawkFragment.QUERYARRAY, queryKeys); + FragmentUtils.replace(activity, PlaylistsFragment.class, bundle); + } + }); + } + + private void setupTextViews(View view) { + if (mAlbum != null) { + View v = ViewUtils.ensureInflation( + view, R.id.album_name_button_stub, R.id.album_name_button); + TextView textView = (TextView) v.findViewById(R.id.textview); + textView.setText(mAlbum.getName()); + v.setOnClickListener(constructAlbumNameClickListener(mAlbum.getCacheKey())); + } else if (mQuery != null || mPlaylistEntry != null || mPlaylist != null + || mStationPlaylist != null) { + View v = ViewUtils.ensureInflation(view, R.id.track_name_stub, R.id.track_name); + TextView textView = (TextView) v; + if (mQuery != null) { + textView.setText(mQuery.getName()); + } else if (mPlaylistEntry != null) { + textView.setText(mPlaylistEntry.getQuery().getPrettyName()); + } else if (mPlaylist != null) { + textView.setText(mPlaylist.getName()); + } else if (mStationPlaylist != null) { + textView.setText(mStationPlaylist.getName()); + } + } + if (mAlbum != null || mQuery != null || mPlaylistEntry != null || mArtist != null) { + View v = ViewUtils.ensureInflation( + view, R.id.artist_name_button_stub, R.id.artist_name_button); + TextView textView = (TextView) v.findViewById(R.id.textview); + String cacheKey; + if (mQuery != null) { + textView.setText(mQuery.getArtist().getPrettyName()); + cacheKey = mQuery.getArtist().getCacheKey(); + } else if (mAlbum != null) { + textView.setText(mAlbum.getArtist().getPrettyName()); + cacheKey = mAlbum.getArtist().getCacheKey(); + } else if (mPlaylistEntry != null) { + textView.setText(mPlaylistEntry.getArtist().getPrettyName()); + cacheKey = mPlaylistEntry.getArtist().getCacheKey(); + } else { + textView.setText(mArtist.getPrettyName()); + cacheKey = mArtist.getCacheKey(); + } + v.setOnClickListener(constructArtistNameClickListener(cacheKey)); + } + } + + private void setupPlaybackTextViews(View view, PlaybackPanel playbackPanel) { + if (mAlbum != null + || (mQuery != null + && !TextUtils.isEmpty(mQuery.getAlbum().getName())) + || (mPlaylistEntry != null + && !TextUtils.isEmpty(mPlaylistEntry.getQuery().getAlbum().getName()))) { + View v = ViewUtils.ensureInflation( + view, R.id.view_album_button_stub, R.id.view_album_button); + TextView viewAlbumButtonText = (TextView) v.findViewById(R.id.textview); + viewAlbumButtonText.setText( + TomahawkApp.getContext().getString(R.string.view_album).toUpperCase()); + String cacheKey; + if (mAlbum != null) { + cacheKey = mAlbum.getCacheKey(); + } else if (mQuery != null) { + cacheKey = mQuery.getAlbum().getCacheKey(); + } else { + cacheKey = mPlaylistEntry.getAlbum().getCacheKey(); + } + v.setOnClickListener(constructAlbumNameClickListener(cacheKey)); + } + if (mAlbum != null || mQuery != null || mPlaylistEntry != null || mArtist != null) { + View artistNameButton = playbackPanel.findViewById(R.id.artist_name_button); + String cacheKey; + if (mAlbum != null) { + cacheKey = mAlbum.getArtist().getCacheKey(); + } else if (mQuery != null) { + cacheKey = mQuery.getArtist().getCacheKey(); + } else if (mPlaylistEntry != null) { + cacheKey = mPlaylistEntry.getArtist().getCacheKey(); + } else { + cacheKey = mArtist.getCacheKey(); + } + artistNameButton.setOnClickListener(constructArtistNameClickListener(cacheKey)); + } + } + + private void setupAlbumArt(View view) { + if (view != null && mAlbum != null + || (mQuery != null && !TextUtils.isEmpty(mQuery.getAlbum().getName())) + || (mPlaylistEntry != null + && !TextUtils.isEmpty(mPlaylistEntry.getQuery().getAlbum().getName()))) { + View v = ViewUtils.ensureInflation(view, R.id.context_menu_albumart_stub, + R.id.context_menu_albumart); + + // load albumart image + ImageView albumImageView = (ImageView) v.findViewById(R.id.album_imageview); + Album album; + String cacheKey; + if (mAlbum != null) { + album = mAlbum; + cacheKey = mAlbum.getCacheKey(); + } else if (mQuery != null) { + album = mQuery.getAlbum(); + cacheKey = mQuery.getAlbum().getCacheKey(); + } else { + album = mPlaylistEntry.getAlbum(); + cacheKey = mPlaylistEntry.getAlbum().getCacheKey(); + } + if (album.getImage() != null) { + ImageUtils.loadImageIntoImageView(TomahawkApp.getContext(), albumImageView, + album.getImage(), Image.getLargeImageSize(), true, false); + } + + // set text on "view album"-button and set up click listener + View viewAlbumButton = view.findViewById(R.id.view_album_button); + TextView viewAlbumButtonText = + (TextView) viewAlbumButton.findViewById(R.id.textview); + viewAlbumButtonText.setText( + TomahawkApp.getContext().getString(R.string.view_album).toUpperCase()); + viewAlbumButton.setOnClickListener(constructAlbumNameClickListener(cacheKey)); + } + } + + private View.OnClickListener constructArtistNameClickListener(final String cacheKey) { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.ARTIST, cacheKey); + if (mCollection != null) { + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + } + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC_PAGER); + bundle.putLong(TomahawkFragment.CONTAINER_FRAGMENT_ID, + IdGenerator.getSessionUniqueId()); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + ArtistPagerFragment.class, bundle); + } + }; + } + + private View.OnClickListener constructAlbumNameClickListener(final String cachekey) { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().getSupportFragmentManager().popBackStack(); + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.ALBUM, cachekey); + if (mCollection != null) { + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + } + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + PlaylistEntriesFragment.class, bundle); + } + }; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/EqualizerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/EqualizerFragment.java new file mode 100644 index 000000000..dcab26a2a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/EqualizerFragment.java @@ -0,0 +1,271 @@ +/***************************************************************************** + * EqualizerFragment.java **************************************************************************** + * Copyright © 2013 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA + * 02110-1301, USA. + *****************************************************************************/ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.mediaplayers.VLCMediaPlayer; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.EqualizerBar; +import org.videolan.libvlc.MediaPlayer; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.v7.widget.SwitchCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.Spinner; + +public class EqualizerFragment extends ContentHeaderFragment { + + public final static String TAG = EqualizerFragment.class.getSimpleName(); + + private SwitchCompat mEnableButton; + + private Spinner mEqualizerPresets; + + private SeekBar mPreAmpSeekBar; + + private LinearLayout mBandsContainers; + + private MediaPlayer.Equalizer mEqualizer = null; + + private final OnItemSelectedListener mPresetListener = new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + mEqualizer = MediaPlayer.Equalizer.createFromPreset(pos); + mPreAmpSeekBar.setProgress((int) mEqualizer.getPreAmp() + 20); + for (int i = 0; i < MediaPlayer.Equalizer.getBandCount(); ++i) { + EqualizerBar bar = (EqualizerBar) mBandsContainers.getChildAt(i); + bar.setValue(mEqualizer.getAmp(i)); + } + if (mEnableButton.isChecked()) { + VLCMediaPlayer.getMediaPlayerInstance().setEqualizer(mEqualizer); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }; + + private final OnSeekBarChangeListener mPreampListener = new OnSeekBarChangeListener() { + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + return; + } + + mEqualizer.setPreAmp(progress - 20); + if (mEnableButton.isChecked()) { + VLCMediaPlayer.getMediaPlayerInstance().setEqualizer(mEqualizer); + } + } + }; + + private class BandListener implements EqualizerBar.OnEqualizerBarChangeListener { + + private final int index; + + public BandListener(int index) { + this.index = index; + } + + @Override + public void onProgressChanged(float value) { + mEqualizer.setAmp(index, value); + if (mEnableButton.isChecked() + && VLCMediaPlayer.getMediaPlayerInstance() != null) { + VLCMediaPlayer.getMediaPlayerInstance().setEqualizer(mEqualizer); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getActivity().setTitle(getResources().getString(R.string.preferences_equalizer)); + + return inflater.inflate(R.layout.equalizerfragment_layout, container, false); + } + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.EqualizerFragment}'s {@link + * android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mEnableButton = (SwitchCompat) view.findViewById(R.id.equalizer_button); + mEqualizerPresets = (Spinner) view.findViewById(R.id.equalizer_presets); + mPreAmpSeekBar = (SeekBar) view.findViewById(R.id.equalizer_preamp); + mBandsContainers = (LinearLayout) view.findViewById(R.id.equalizer_bands); + setupNonScrollableSpacer(getView()); + } + + @Override + public void onResume() { + super.onResume(); + + fillViews(); + } + + @Override + public void onPause() { + super.onPause(); + + mEnableButton.setOnCheckedChangeListener(null); + mEqualizerPresets.setOnItemSelectedListener(null); + mPreAmpSeekBar.setOnSeekBarChangeListener(null); + mBandsContainers.removeAllViews(); + + if (mEnableButton.isChecked()) { + storeEqualizerSettings(mEqualizer, mEqualizerPresets.getSelectedItemPosition()); + } else { + storeEqualizerSettings(null, 0); + } + } + + private void fillViews() { + final Context context = getActivity(); + + if (context == null) { + return; + } + + final String[] presets = getEqualizerPresets(); + + mEqualizer = readEqualizerSettings(); + final boolean isEnabled = mEqualizer != null; + if (mEqualizer == null) { + mEqualizer = MediaPlayer.Equalizer.create(); + } + + // on/off + mEnableButton.setChecked(isEnabled); + mEnableButton.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (VLCMediaPlayer.getMediaPlayerInstance() != null) { + VLCMediaPlayer.getMediaPlayerInstance().setEqualizer( + isChecked ? mEqualizer : null); + } + } + }); + + // presets + mEqualizerPresets.setAdapter(new ArrayAdapter<>(getActivity(), + android.R.layout.simple_spinner_dropdown_item, presets)); + + // Set the default selection asynchronously to prevent a layout initialization bug. + final int equalizer_preset_pref = PreferenceUtils.getInt(PreferenceUtils.EQUALIZER_PRESET); + mEqualizerPresets.post(new Runnable() { + @Override + public void run() { + mEqualizerPresets.setSelection(equalizer_preset_pref, false); + mEqualizerPresets.setOnItemSelectedListener(mPresetListener); + } + }); + + // preamp + mPreAmpSeekBar.setMax(40); + mPreAmpSeekBar.setProgress((int) mEqualizer.getPreAmp() + 20); + mPreAmpSeekBar.setOnSeekBarChangeListener(mPreampListener); + + // bands + for (int i = 0; i < MediaPlayer.Equalizer.getBandCount(); i++) { + float band = MediaPlayer.Equalizer.getBandFrequency(i); + + EqualizerBar bar = new EqualizerBar(getActivity(), band); + bar.setValue(mEqualizer.getAmp(i)); + bar.setListener(new BandListener(i)); + + mBandsContainers.addView(bar); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, 1); + bar.setLayoutParams(params); + } + } + + private static String[] getEqualizerPresets() { + final int count = MediaPlayer.Equalizer.getPresetCount(); + final String[] presets = new String[count]; + for (int i = 0; i < count; ++i) { + presets[i] = MediaPlayer.Equalizer.getPresetName(i); + } + return presets; + } + + @MainThread + public static MediaPlayer.Equalizer readEqualizerSettings() { + if (PreferenceUtils.getBoolean(PreferenceUtils.EQUALIZER_ENABLED)) { + final float[] bands = PreferenceUtils.getFloatArray(PreferenceUtils.EQUALIZER_VALUES); + final int bandCount = MediaPlayer.Equalizer.getBandCount(); + if (bands.length != bandCount + 1) { + return null; + } + + final MediaPlayer.Equalizer eq = MediaPlayer.Equalizer.create(); + eq.setPreAmp(bands[0]); + for (int i = 0; i < bandCount; ++i) { + eq.setAmp(i, bands[i + 1]); + } + return eq; + } else { + return null; + } + } + + public static void storeEqualizerSettings(MediaPlayer.Equalizer eq, int preset) { + SharedPreferences.Editor editor = PreferenceUtils.edit(); + if (eq != null) { + editor.putBoolean(PreferenceUtils.EQUALIZER_ENABLED, true); + final int bandCount = MediaPlayer.Equalizer.getBandCount(); + final float[] bands = new float[bandCount + 1]; + bands[0] = eq.getPreAmp(); + for (int i = 0; i < bandCount; ++i) { + bands[i + 1] = eq.getAmp(i); + } + PreferenceUtils.putFloatArray(editor, PreferenceUtils.EQUALIZER_VALUES, bands); + editor.putInt(PreferenceUtils.EQUALIZER_PRESET, preset); + } else { + editor.putBoolean(PreferenceUtils.EQUALIZER_ENABLED, false); + } + editor.apply(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PagerFragment.java new file mode 100644 index 000000000..24cc0aaca --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PagerFragment.java @@ -0,0 +1,285 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.TomahawkPagerAdapter; +import org.tomahawk.tomahawk_android.listeners.TomahawkPanelSlideListener; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.views.PageIndicator; +import org.tomahawk.tomahawk_android.views.Selector; + +import android.os.Bundle; +import android.support.v4.view.ViewPager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import de.greenrobot.event.EventBus; + +public abstract class PagerFragment extends ContentHeaderFragment implements + ViewPager.OnPageChangeListener { + + private final static String TAG = PagerFragment.class.getSimpleName(); + + protected final HashSet mCorrespondingRequestIds = new HashSet<>(); + + private ViewPager mViewPager; + + private boolean mShouldSyncListStates = true; + + private PageIndicator mPageIndicator; + + public class FragmentInfoList { + + private List mFragmentInfos; + + private int mCurrent = 0; + + public void addFragmentInfo(FragmentInfo fragmentInfo) { + if (mFragmentInfos == null) { + mFragmentInfos = new ArrayList<>(); + } + mFragmentInfos.add(fragmentInfo); + } + + public List getFragmentInfos() { + return mFragmentInfos; + } + + public FragmentInfo getCurrentFragmentInfo() { + return mFragmentInfos.get(mCurrent); + } + + public void setCurrent(int current) { + mCurrent = current; + } + + public int size() { + return mFragmentInfos.size(); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(AnimateEvent event) { + if (mContainerFragmentId == event.mContainerFragmentId + && mViewPager != null + && event.mContainerFragmentPage == mViewPager.getCurrentItem()) { + animate(event.mPlayTime); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(InfoSystem.ResultsEvent event) { + onInfoSystemResultsReported(event.mInfoRequestData); + } + + @SuppressWarnings("unused") + public void onEventMainThread(TomahawkPanelSlideListener.SlidingLayoutChangedEvent event) { + switch (event.mSlideState) { + case COLLAPSED: + case EXPANDED: + onSlidingLayoutShown(); + break; + case HIDDEN: + onSlidingLayoutHidden(); + break; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.pagerfragment_layout, container, false); + } + + /** + * This method will be invoked when the current page is scrolled, either as part of a + * programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. Page + * position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at + * position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mPageIndicator != null) { + mPageIndicator.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + if (mShouldSyncListStates && positionOffset != 0f && isDynamicHeader()) { + mShouldSyncListStates = false; + RequestSyncEvent event = new RequestSyncEvent(); + if (mViewPager.getCurrentItem() == position) { + // first visible fragment is the current fragment, + // so we get the one to the right by asking for the fragment at position + 1 + event.mReceiverFragmentPage = position + 1; + } else { + // first visible fragment is the left fragment + event.mReceiverFragmentPage = position; + } + event.mPerformerFragmentPage = mViewPager.getCurrentItem(); + event.mContainerFragmentId = mContainerFragmentId; + EventBus.getDefault().post(event); + } + } + + /** + * This method will be invoked when a new page becomes selected. Animation is not necessarily + * complete. + * + * @param position Position index of the new selected page. + */ + @Override + public void onPageSelected(int position) { + if (mPageIndicator != null) { + mPageIndicator.onPageSelected(position); + } + } + + /** + * Called when the scroll state changes. Useful for discovering when the user begins dragging, + * when the pager is automatically settling to the current page, or when it is fully + * stopped/idle. + * + * @param state The new scroll state. + * @see ViewPager#SCROLL_STATE_IDLE + * @see ViewPager#SCROLL_STATE_DRAGGING + * @see ViewPager#SCROLL_STATE_SETTLING + */ + @Override + public void onPageScrollStateChanged(int state) { + if (mPageIndicator != null) { + mPageIndicator.onPageScrollStateChanged(state); + } + + if (state == ViewPager.SCROLL_STATE_IDLE) { + mShouldSyncListStates = true; + } + } + + protected void setupPager(List fragmentInfoLists, int initialPage, + String selectorPosStorageKey, int offscreenPageLimit) { + if (getView() == null) { + return; + } + + View loadingIndicator = getView().findViewById(R.id.circularprogressview_pager); + loadingIndicator.setVisibility(View.GONE); + + fillAdapter(fragmentInfoLists, initialPage, offscreenPageLimit); + + mPageIndicator = (PageIndicator) getView().findViewById(R.id.page_indicator); + mPageIndicator.setVisibility(View.VISIBLE); + mPageIndicator.setup(mViewPager, fragmentInfoLists, + getActivity().findViewById(R.id.sliding_layout), + (Selector) getView().findViewById(R.id.selector), selectorPosStorageKey); + if (((TomahawkMainActivity) getActivity()).getSlidingUpPanelLayout().getPanelState() + == SlidingUpPanelLayout.PanelState.HIDDEN) { + onSlidingLayoutHidden(); + } else { + onSlidingLayoutShown(); + } + } + + protected void fillAdapter(List fragmentInfoLists, int initialPage, + int offscreenPageLimit) { + if (getView() == null) { + return; + } + + List currentFragmentInfos = new ArrayList<>(); + for (FragmentInfoList list : fragmentInfoLists) { + currentFragmentInfos.add(list.getCurrentFragmentInfo()); + } + mViewPager = (ViewPager) getView().findViewById(R.id.fragmentpager); + mViewPager.addOnPageChangeListener(this); + mViewPager.setOffscreenPageLimit(offscreenPageLimit); + if (initialPage >= 0) { + mViewPager.setCurrentItem(initialPage); + } + if (mViewPager.getAdapter() == null) { + TomahawkPagerAdapter pagerAdapter = new TomahawkPagerAdapter(getChildFragmentManager(), + currentFragmentInfos, ((Object) this).getClass(), mContainerFragmentId); + mViewPager.setAdapter(pagerAdapter); + } else { + TomahawkPagerAdapter pagerAdapter = (TomahawkPagerAdapter) mViewPager.getAdapter(); + pagerAdapter.changeFragments(currentFragmentInfos); + } + } + + protected void onInfoSystemResultsReported(InfoRequestData infoRequestData) { + } + + protected void showContentHeader(Object item) { + super.showContentHeader(item); + super.setupAnimations(); + super.setupNonScrollableSpacer(getView().findViewById(R.id.selector)); + } + + private void onSlidingLayoutShown() { + if (getView() != null) { + Selector selector = (Selector) getView().findViewById(R.id.selector); + if (selector != null) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) selector + .getLayoutParams(); + params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, + ((TomahawkMainActivity) getActivity()).getSlidingUpPanelLayout() + .getPanelHeight() * -1); + selector.setLayoutParams(params); + } + } + } + + private void onSlidingLayoutHidden() { + if (getView() != null) { + Selector selector = (Selector) getView().findViewById(R.id.selector); + if (selector != null) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) selector + .getLayoutParams(); + params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, 0); + selector.setLayoutParams(params); + } + } + } + + protected Bundle getChildFragmentBundle() { + Bundle bundle = new Bundle(); + if (getArguments().containsKey(TomahawkFragment.COLLECTION_ID)) { + bundle.putString(TomahawkFragment.COLLECTION_ID, + getArguments().getString(TomahawkFragment.COLLECTION_ID)); + } + if (getArguments().containsKey(TomahawkFragment.CONTENT_HEADER_MODE)) { + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + getArguments().getInt(TomahawkFragment.CONTENT_HEADER_MODE)); + } + return bundle; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaybackFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaybackFragment.java new file mode 100644 index 000000000..4633ccf8b --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaybackFragment.java @@ -0,0 +1,665 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.ValueAnimator; +import com.sothree.slidinguppanel.SlidingUpPanelLayout; +import com.squareup.picasso.Callback; + +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.AlbumArtSwipeAdapter; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.listeners.TomahawkPanelSlideListener; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.AlbumArtViewPager; +import org.tomahawk.tomahawk_android.views.PlaybackFragmentFrame; + +import android.animation.Animator; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * This {@link android.support.v4.app.Fragment} represents our Playback view in which the user can + * play/stop/pause. It is being shown as the topmost fragment in the {@link PlaybackFragment}'s + * {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView}. + */ +public class PlaybackFragment extends TomahawkFragment { + + private static final String TAG = PlaybackFragment.class.getSimpleName(); + + private AlbumArtSwipeAdapter mAlbumArtSwipeAdapter; + + private AlbumArtViewPager mAlbumArtViewPager; + + private FrameLayout mAlbumArtViewPagerFrame; + + private ImageView mSwipeHintLeft; + + private ImageView mSwipeHintRight; + + private ImageView mSwipeHintBottom; + + private boolean mSwipeHintsShown = false; + + private boolean mShouldShowSwipeHints = false; + + private int mOriginalViewPagerHeight; + + private Image mCurrentBlurredImage; + + private final GestureDetector.SimpleOnGestureListener mGestureListener + = new GestureDetector.SimpleOnGestureListener() { + + @Override + public void onLongPress(MotionEvent e) { + if (getMediaController() == null) { + Log.d(TAG, "onLongPress failed because getMediaController() is null"); + return; + } + FragmentUtils.showContextMenu((TomahawkMainActivity) getActivity(), + getPlaybackManager().getCurrentEntry().getQuery(), null, true, true); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (getMediaController() == null) { + Log.e(TAG, "onDoubleTap failed because getMediaController() is null"); + return false; + } + if (getPlaybackManager().getCurrentQuery() == null) { + Log.e(TAG, "onDoubleTap failed because getPlaybackManager().getCurrentQuery()" + + " is null"); + return false; + } + final ImageView imageView = + (ImageView) getView().findViewById(R.id.imageview_favorite_doubletap); + if (DatabaseHelper.get().isItemLoved(getPlaybackManager().getCurrentQuery())) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_action_unfavorite_large); + } else { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, + R.drawable.ic_action_favorite_large); + } + AnimationUtils.fade(imageView, AnimationUtils.DURATION_CONTEXTMENU, true); + Runnable r = new Runnable() { + @Override + public void run() { + AnimationUtils.fade(imageView, AnimationUtils.DURATION_CONTEXTMENU, false); + } + }; + new Handler().postDelayed(r, 2000); + CollectionManager.get().toggleLovedItem(getPlaybackManager().getCurrentQuery()); + return false; + } + }; + + @SuppressWarnings("unused") + public void onEventMainThread(InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + mAlbumArtSwipeAdapter.updatePlaylist(); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(TomahawkPanelSlideListener.SlidingLayoutChangedEvent event) { + if (event.mSlideState == SlidingUpPanelLayout.PanelState.EXPANDED + || event.mSlideState == SlidingUpPanelLayout.PanelState.COLLAPSED) { + if (mAlbumArtSwipeAdapter != null) { + mAlbumArtSwipeAdapter.notifyDataSetChanged(); + } + if (getListView() != null) { + getListView().smoothScrollToPosition(0); + } + } + if (event.mSlideState == SlidingUpPanelLayout.PanelState.EXPANDED) { + if (mShouldShowSwipeHints) { + mShouldShowSwipeHints = false; + if (getMediaController() != null) { + showSwipeHints(getMediaController().getPlaybackState()); + } + } + } + if (event.mSlideState == SlidingUpPanelLayout.PanelState.COLLAPSED) { + mShouldShowSwipeHints = true; + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRestoreScrollPosition(false); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.playback_fragment, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(view) { + @Override + public void run() { + if (getListView() != null) { + mHeaderScrollableHeight = + getLayedOutView().getHeight() - mHeaderNonscrollableHeight; + setupScrollableSpacer(getListAdapter(), getListView(), mAlbumArtViewPager); + setupNonScrollableSpacer(getListView()); + } + } + }); + + getListView().setFastScrollEnabled(false); + + mSwipeHintLeft = (ImageView) view.findViewById(R.id.swipe_hint_left); + mSwipeHintRight = (ImageView) view.findViewById(R.id.swipe_hint_right); + mSwipeHintBottom = (ImageView) view.findViewById(R.id.swipe_hint_bottom); + + mAlbumArtViewPagerFrame = (FrameLayout) view.findViewById(R.id.albumart_viewpager_frame); + + mAlbumArtViewPager = (AlbumArtViewPager) + mAlbumArtViewPagerFrame.findViewById(R.id.albumart_viewpager); + int padding = getResources().getDimensionPixelSize(R.dimen.padding_large); + mAlbumArtViewPager.setPadding(padding, 0, padding, 0); + mAlbumArtViewPager.setClipToPadding(false); + padding = getResources().getDimensionPixelSize(R.dimen.padding_large); + mAlbumArtViewPager.setPageMargin(padding); + mAlbumArtViewPager.setOnGestureListener(mGestureListener); + + PlaybackFragmentFrame playbackFragmentFrame = (PlaybackFragmentFrame) view.getParent(); + playbackFragmentFrame.setListView(getListView()); + playbackFragmentFrame.setPanelLayout( + ((TomahawkMainActivity) getActivity()).getSlidingUpPanelLayout()); + + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + + //Set listeners on our buttons + view.findViewById(R.id.imageButton_shuffle).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onShuffleClicked(); + } + }); + view.findViewById(R.id.imageButton_repeat).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onRepeatClicked(); + } + }); + View closeButton = view.findViewById(R.id.close_button); + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SlidingUpPanelLayout slidingLayout = + ((TomahawkMainActivity) getActivity()).getSlidingUpPanelLayout(); + slidingLayout.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + }); + TextView closeButtonText = (TextView) closeButton.findViewById(R.id.close_button_text); + closeButtonText.setText(getString(R.string.button_close).toUpperCase()); + + if (!PreferenceUtils.getBoolean( + PreferenceUtils.COACHMARK_PLAYBACKFRAGMENT_NAVIGATION_DISABLED)) { + final View coachMark = ViewUtils.ensureInflation(view, + R.id.playbackfragment_navigation_coachmark_stub, + R.id.playbackfragment_navigation_coachmark); + coachMark.findViewById(R.id.close_button).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + coachMark.setVisibility(View.GONE); + } + }); + coachMark.setVisibility(View.VISIBLE); + } + } + + @Override + public void onResume() { + super.onResume(); + + mAlbumArtSwipeAdapter = new AlbumArtSwipeAdapter(mAlbumArtViewPager); + mAlbumArtViewPager.setAdapter(mAlbumArtSwipeAdapter); + + setupAlbumArtAnimation(); + + refreshAll(); + } + + private void refreshAll() { + if (getMediaController() != null && mAlbumArtSwipeAdapter != null) { + mAlbumArtSwipeAdapter.setMediaController(getMediaController()); + mAlbumArtSwipeAdapter.setPlaybackManager(getPlaybackManager()); + mAlbumArtSwipeAdapter.updatePlaylist(); + refreshTrackInfo(getMediaController().getMetadata()); + refreshRepeatButtonState(getMediaController().getPlaybackState()); + refreshShuffleButtonState(getMediaController().getPlaybackState()); + updateAdapter(); + } + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (getMediaController() == null) { + Log.d(TAG, "onItemClick failed because getMediaController() is null"); + return; + } + if (item instanceof PlaylistEntry) { + PlaylistEntry entry = (PlaylistEntry) item; + if (entry.getQuery().isPlayable()) { + if (getPlaybackManager().getCurrentEntry() == entry) { + // if the user clicked on an already playing track + int playState = getMediaController().getPlaybackState().getState(); + if (playState == PlaybackStateCompat.STATE_PLAYING) { + getMediaController().getTransportControls().pause(); + } else if (playState == PlaybackStateCompat.STATE_PAUSED) { + getMediaController().getTransportControls().play(); + } + } else { + getPlaybackManager().setCurrentEntry(entry); + } + } + } + } + + /** + * Called every time an item inside a ListView or GridView is long-clicked + * + * @param item the Object which corresponds to the long-click + * @param segment + */ + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + if (item != null) { + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + AnimationUtils + .fade(activity.getPlaybackPanel(), AnimationUtils.DURATION_CONTEXTMENU, false); + return FragmentUtils + .showContextMenu((TomahawkMainActivity) getActivity(), item, null, false, true); + } + return false; + } + + @Override + public void onMediaControllerConnected() { + super.onMediaControllerConnected(); + + refreshAll(); + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + refreshTrackInfo(metadata); + scheduleUpdateAdapter(); + } + + @Override + public void onQueueChanged(List queue) { + forceResolveVisibleItems(false); + scheduleUpdateAdapter(); + + if (getMediaController() != null) { + refreshRepeatButtonState(getMediaController().getPlaybackState()); + refreshShuffleButtonState(getMediaController().getPlaybackState()); + } + if (mAlbumArtSwipeAdapter != null) { + mAlbumArtSwipeAdapter.updatePlaylist(); + } + } + + @Override + public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { + if (getMediaController() != null) { + refreshRepeatButtonState(state); + refreshShuffleButtonState(state); + } + if (mAlbumArtSwipeAdapter != null) { + mAlbumArtSwipeAdapter.updatePlaylist(); + } + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + Log.e(TAG, "updateAdapter failed because Fragment is not resumed"); + return; + } + if (getMediaController() == null) { + Log.e(TAG, "updateAdapter failed because getMediaController() is null"); + return; + } + + if (getPlaybackManager() != null) { + List segments = new ArrayList<>(); + Segment segment = new Segment.Builder(getPlaybackManager()) + .showNumeration(true, 0) + .build(); + segments.add(segment); + fillAdapter(segments, mAlbumArtViewPager, null); + } + } + + private void setupAlbumArtAnimation() { + if (mAlbumArtViewPagerFrame != null) { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(mAlbumArtViewPagerFrame) { + @Override + public void run() { + if (mOriginalViewPagerHeight <= 0) { + mOriginalViewPagerHeight = mAlbumArtViewPager.getHeight(); + } + + // now calculate the animation goal and instantiate the animation + int playbackPanelHeight = TomahawkApp.getContext().getResources() + .getDimensionPixelSize(R.dimen.playback_panel_height); + ValueAnimator animator = ObjectAnimator + .ofFloat(getLayedOutView(), "y", playbackPanelHeight, + getLayedOutView().getHeight() / -4) + .setDuration(10000); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + addAnimator(ANIM_ALBUMART_ID, animator); + + refreshAnimations(); + } + }); + } + } + + /** + * Called when the shuffle button is clicked. + */ + public void onShuffleClicked() { + if (getMediaController() == null) { + Log.e(TAG, "onShuffleClicked failed because getMediaController() is null"); + return; + } + Bundle playbackStateExtras = getMediaController().getPlaybackState().getExtras(); + if (playbackStateExtras != null) { + int shuffleMode = playbackStateExtras.getInt(PlaybackService.EXTRAS_KEY_SHUFFLE_MODE); + int newShuffleMode = shuffleMode == PlaybackManager.SHUFFLED + ? PlaybackManager.NOT_SHUFFLED : PlaybackManager.SHUFFLED; + Bundle extras = new Bundle(); + extras.putInt(PlaybackService.EXTRAS_KEY_SHUFFLE_MODE, newShuffleMode); + getMediaController().getTransportControls() + .sendCustomAction(PlaybackService.ACTION_SET_SHUFFLE_MODE, extras); + } + } + + /** + * Called when the repeat button is clicked. + */ + public void onRepeatClicked() { + if (getMediaController() == null) { + Log.e(TAG, "onRepeatClicked failed because getMediaController() is null"); + return; + } + Bundle playbackStateExtras = getMediaController().getPlaybackState().getExtras(); + if (playbackStateExtras != null) { + int repeatMode = playbackStateExtras.getInt(PlaybackService.EXTRAS_KEY_REPEAT_MODE); + int newRepeatMode = PlaybackManager.NOT_REPEATING; + if (repeatMode == PlaybackManager.NOT_REPEATING) { + newRepeatMode = PlaybackManager.REPEAT_ALL; + } else if (repeatMode == PlaybackManager.REPEAT_ALL) { + newRepeatMode = PlaybackManager.REPEAT_ONE; + } else if (repeatMode == PlaybackManager.REPEAT_ONE) { + newRepeatMode = PlaybackManager.NOT_REPEATING; + } + Bundle extras = new Bundle(); + extras.putInt(PlaybackService.EXTRAS_KEY_REPEAT_MODE, newRepeatMode); + getMediaController().getTransportControls() + .sendCustomAction(PlaybackService.ACTION_SET_REPEAT_MODE, extras); + } + } + + /** + * Refresh the information in this fragment to reflect that of the given Track. + */ + protected void refreshTrackInfo(MediaMetadataCompat metadata) { + if (getView() != null && metadata != null + && getPlaybackManager().getCurrentQuery() != null) { + if (getPlaybackManager().getPreviousEntry() != null) { + resolveImages(getPlaybackManager().getPreviousEntry().getQuery()); + } + if (getPlaybackManager().getNextEntry() != null) { + resolveImages(getPlaybackManager().getNextEntry().getQuery()); + } + if (mCurrentBlurredImage != getPlaybackManager().getCurrentQuery().getImage()) { + mCurrentBlurredImage = getPlaybackManager().getCurrentQuery().getImage(); + ImageView bgImageView = (ImageView) getView().findViewById(R.id.background); + ImageView bgAltImageView = (ImageView) getView().findViewById(R.id.background_alt); + final ImageView imageViewToFadeIn; + final ImageView imageViewToFadeOut; + if (bgAltImageView.getAlpha() < bgImageView.getAlpha()) { + imageViewToFadeIn = bgAltImageView; + imageViewToFadeOut = bgImageView; + } else { + imageViewToFadeIn = bgImageView; + imageViewToFadeOut = bgAltImageView; + } + Callback fadeCallback = new Callback() { + @Override + public void onSuccess() { + AnimationUtils.fade(imageViewToFadeIn, imageViewToFadeIn.getAlpha(), + 1f, AnimationUtils.DURATION_PLAYBACKFRAGMENT_BG, true, null); + AnimationUtils.fade(imageViewToFadeOut, imageViewToFadeOut.getAlpha(), + 0f, AnimationUtils.DURATION_PLAYBACKFRAGMENT_BG, false, + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + imageViewToFadeOut.setImageDrawable(null); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } + + @Override + public void onError() { + } + }; + if (mCurrentBlurredImage != null) { + ImageUtils.loadBlurredImageIntoImageView(TomahawkApp.getContext(), + imageViewToFadeIn, mCurrentBlurredImage, Image.getSmallImageSize(), + 0, fadeCallback); + } else { + imageViewToFadeIn.setImageDrawable(new ColorDrawable( + getResources().getColor(R.color.playerview_default_bg))); + fadeCallback.onSuccess(); + } + } + } + } + + private void resolveImages(Query query) { + if (query.getImage() == null) { + String requestId = InfoSystem.get().resolve(query.getArtist(), false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + requestId = InfoSystem.get().resolve(query.getAlbum()); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + + /** + * Refresh the information in this fragment to reflect that of the current repeatButton state. + */ + protected void refreshRepeatButtonState(PlaybackStateCompat playbackState) { + if (getView() != null) { + ImageButton imageButton = (ImageButton) getView().findViewById(R.id.imageButton_repeat); + if (imageButton != null) { + if (playbackState.getExtras() != null + && !(getPlaybackManager().getPlaylist() instanceof StationPlaylist) + && getPlaybackManager().getCurrentEntry() != null) { + imageButton.setAlpha(1f); + imageButton.setClickable(true); + int repeatMode = playbackState.getExtras() + .getInt(PlaybackService.EXTRAS_KEY_REPEAT_MODE); + if (repeatMode == PlaybackManager.REPEAT_ALL) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), + imageButton, R.drawable.repeat_all, R.color.tomahawk_red); + } else if (repeatMode == PlaybackManager.REPEAT_ONE) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), + imageButton, R.drawable.repeat_one, R.color.tomahawk_red); + } else if (repeatMode == PlaybackManager.NOT_REPEATING) { + ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), + imageButton, R.drawable.repeat_all); + } + } else { + imageButton.setAlpha(0.2f); + imageButton.setClickable(false); + } + } + } + } + + /** + * Refresh the information in this fragment to reflect that of the current shuffleButton state. + */ + protected void refreshShuffleButtonState(PlaybackStateCompat playbackState) { + if (getView() != null) { + ImageButton imageButton = + (ImageButton) getView().findViewById(R.id.imageButton_shuffle); + if (imageButton != null) { + if (playbackState.getExtras() != null + && !(getPlaybackManager().getPlaylist() instanceof StationPlaylist) + && getPlaybackManager().getCurrentEntry() != null) { + imageButton.setAlpha(1f); + imageButton.setClickable(true); + int repeatMode = playbackState.getExtras() + .getInt(PlaybackService.EXTRAS_KEY_SHUFFLE_MODE); + if (repeatMode == PlaybackManager.SHUFFLED) { + ImageUtils.setTint(imageButton.getDrawable(), R.color.tomahawk_red); + } else if (repeatMode == PlaybackManager.NOT_SHUFFLED) { + ImageUtils.clearTint(imageButton.getDrawable()); + } + } else { + imageButton.setAlpha(0.2f); + imageButton.setClickable(false); + } + } + } + } + + @Override + public void animate(int position) { + super.animate(position); + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + if (activity.getSlidingOffset() > 0f) { + activity.getPlaybackPanel().animate(position + 10000); + } + } + + public void showSwipeHints(PlaybackStateCompat playbackState) { + if (!mSwipeHintsShown && mSwipeHintLeft != null && mSwipeHintRight != null + && mSwipeHintBottom != null) { + mSwipeHintsShown = true; + AnimationUtils.fade(mSwipeHintBottom, AnimationUtils.DURATION_PLAYBACKTOPPANEL, true); + long actions = playbackState.getActions(); + final boolean hasPreviousEntry = + (actions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0; + final boolean hasNextEntry = + (actions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0; + if (hasPreviousEntry) { + AnimationUtils.fade(mSwipeHintLeft, AnimationUtils.DURATION_PLAYBACKTOPPANEL, true); + } + if (hasNextEntry) { + AnimationUtils.fade( + mSwipeHintRight, AnimationUtils.DURATION_PLAYBACKTOPPANEL, true); + } + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + mSwipeHintsShown = false; + if (hasPreviousEntry) { + AnimationUtils.fade( + mSwipeHintLeft, AnimationUtils.DURATION_PLAYBACKTOPPANEL, false); + } + if (hasNextEntry) { + AnimationUtils.fade( + mSwipeHintRight, AnimationUtils.DURATION_PLAYBACKTOPPANEL, false); + } + AnimationUtils.fade( + mSwipeHintBottom, AnimationUtils.DURATION_PLAYBACKTOPPANEL, false); + } + }, 1500); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistEntriesFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistEntriesFragment.java new file mode 100644 index 000000000..8f52a703d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistEntriesFragment.java @@ -0,0 +1,344 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; +import org.tomahawk.tomahawk_android.views.FancyDropDown; + +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment} which shows a set of {@link + * org.tomahawk.libtomahawk.collection.Track}s inside its {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ +public class PlaylistEntriesFragment extends TomahawkFragment { + + private static final String TAG = PlaylistEntriesFragment.class.getSimpleName(); + + public static final int SHOW_MODE_LOVEDITEMS = 0; + + public static final int SHOW_MODE_PLAYBACKLOG = 1; + + public static final String COLLECTION_TRACKS_SPINNER_POSITION + = "org.tomahawk.tomahawk_android.collection_tracks_spinner_position_"; + + private Set mResolvingTopArtistNames = new HashSet<>(); + + private Playlist mCurrentPlaylist; + + @SuppressWarnings("unused") + public void onEvent(DatabaseHelper.PlaylistsUpdatedEvent event) { + if (mPlaylist != null && mPlaylist.getId().equals(event.mPlaylistId)) { + mPlaylist = DatabaseHelper.get().getPlaylist(mPlaylist.getId()); + scheduleUpdateAdapter(); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(CollectionManager.UpdatedEvent event) { + super.onEventMainThread(event); + + if (event.mUpdatedItemIds != null && event.mUpdatedItemIds.contains(mAlbum.getCacheKey()) + && mContainerFragmentClass == null) { + showAlbumFancyDropDown(); + } + } + + @Override + public void onResume() { + super.onResume(); + + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (mUser != null) { + if (mShowMode == SHOW_MODE_PLAYBACKLOG) { + String requestId = InfoSystem.get().resolvePlaybackLog(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } else if (mShowMode == SHOW_MODE_LOVEDITEMS) { + mHideRemoveButton = true; + if (mUser == user) { + CollectionManager.get().fetchLovedItemsPlaylist(); + } else { + String requestId = InfoSystem.get().resolveLovedItems(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + if (mUser != user) { + mHideRemoveButton = true; + } else { + CollectionManager.get().fetchPlaylists(); + } + } else { + mHideRemoveButton = true; + } + updateAdapter(); + } + }); + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (getMediaController() == null) { + Log.e(TAG, "onItemClick failed because getMediaController() is null"); + return; + } + if (item instanceof PlaylistEntry) { + PlaylistEntry entry = (PlaylistEntry) item; + if (entry.getQuery().isPlayable()) { + if (getPlaybackManager().getCurrentEntry() == entry) { + // if the user clicked on an already playing track + int playState = getMediaController().getPlaybackState().getState(); + if (playState == PlaybackStateCompat.STATE_PLAYING) { + getMediaController().getTransportControls().pause(); + } else if (playState == PlaybackStateCompat.STATE_PAUSED) { + getMediaController().getTransportControls().play(); + } + } else { + getPlaybackManager().setPlaylist(mCurrentPlaylist, entry); + getMediaController().getTransportControls().play(); + } + } + } + } + + /** + * Update this {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment}'s {@link + * org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + if (mAlbum != null) { + showContentHeader(mAlbum); + if (mContainerFragmentClass == null) { + showAlbumFancyDropDown(); + } + mCollection.getAlbumTracks(mAlbum).done(new DoneCallback() { + @Override + public void onDone(Playlist playlist) { + mCurrentPlaylist = playlist; + Segment.Builder builder = new Segment.Builder(playlist) + .headerLayout(R.layout.single_line_list_header) + .headerString(mAlbum.getArtist().getPrettyName()); + if (playlist != null && playlist.allFromOneArtist()) { + builder.hideArtistName(true); + builder.showDuration(true); + } + builder.showNumeration(true, 1); + fillAdapter(builder.build()); + } + }); + } else if (mUser != null || mPlaylist != null) { + if (mUser != null) { + if (mShowMode == SHOW_MODE_PLAYBACKLOG) { + mCurrentPlaylist = mUser.getPlaybackLog(); + } else if (mShowMode == SHOW_MODE_LOVEDITEMS) { + mCurrentPlaylist = mUser.getFavorites(); + } + } + if (mPlaylist != null) { + mCurrentPlaylist = mPlaylist; + } + if (!mCurrentPlaylist.isFilled()) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (mShowMode < 0) { + if (mUser != user) { + String requestId = InfoSystem.get().resolve(mCurrentPlaylist); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } else if (mPlaylist != null) { + mPlaylist = DatabaseHelper.get().getPlaylist(mPlaylist.getId()); + updateAdapter(); + } + } + } + }); + } else { + Segment.Builder builder = new Segment.Builder(mCurrentPlaylist); + if (mContainerFragmentClass == null + || !mContainerFragmentClass.equals(SearchPagerFragment.class.getName())) { + builder.showNumeration(true, 1); + if (mContainerFragmentClass == null || !mContainerFragmentClass + .equals(ChartsPagerFragment.class.getName())) { + builder.headerLayout(R.layout.single_line_list_header) + .headerString(R.string.playlist_details); + } + } else if (mContainerFragmentClass.equals(SearchPagerFragment.class.getName())) { + builder.showResolverIcon(true); + } + Segment segment = builder.build(); + fillAdapter(segment); + showContentHeader(mCurrentPlaylist); + showFancyDropDown(0, mCurrentPlaylist.getName(), null, null); + ThreadManager.get() + .execute(new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_INFOSYSTEM_LOW) { + @Override + public void run() { + if (mCurrentPlaylist.getTopArtistNames() == null + || mCurrentPlaylist.getTopArtistNames().length == 0) { + boolean isFavorites = mUser != null + && mCurrentPlaylist == mUser.getFavorites(); + mCurrentPlaylist.updateTopArtistNames(isFavorites); + } else { + for (int i = 0; i < mCurrentPlaylist.getTopArtistNames().length + && i < 5; i++) { + String artistName = mCurrentPlaylist.getTopArtistNames()[i]; + if (!mResolvingTopArtistNames.contains(artistName)) { + String requestId = InfoSystem.get() + .resolve(Artist.get(artistName), false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + mResolvingTopArtistNames.add(artistName); + } + } + } + } + }); + } + } else { + mCollection.getQueries(getSortMode()).done(new DoneCallback() { + @Override + public void onDone(final Playlist playlist) { + new Thread(new Runnable() { + @Override + public void run() { + mCurrentPlaylist = playlist; + String id = mCollection.getId(); + Segment segment = new Segment.Builder(playlist) + .headerLayout(R.layout.dropdown_header) + .headerStrings(constructDropdownItems()) + .spinner(constructDropdownListener( + COLLECTION_TRACKS_SPINNER_POSITION + id), + getDropdownPos(COLLECTION_TRACKS_SPINNER_POSITION + id)) + .build(); + fillAdapter(segment); + } + }).start(); + } + }); + } + } + + private List constructDropdownItems() { + List dropDownItems = new ArrayList<>(); + if (!(mCollection instanceof ScriptResolverCollection)) { + dropDownItems.add(R.string.collection_dropdown_recently_added); + } + dropDownItems.add(R.string.collection_dropdown_alpha); + dropDownItems.add(R.string.collection_dropdown_alpha_artists); + return dropDownItems; + } + + private int getSortMode() { + String id = mCollection.getId(); + int pos = getDropdownPos(COLLECTION_TRACKS_SPINNER_POSITION + id); + if (!(mCollection instanceof ScriptResolverCollection)) { + switch (pos) { + case 0: + return Collection.SORT_LAST_MODIFIED; + case 1: + return Collection.SORT_ALPHA; + case 2: + return Collection.SORT_ARTIST_ALPHA; + default: + return Collection.SORT_NOT; + } + } else { + switch (pos) { + case 0: + return Collection.SORT_ALPHA; + case 1: + return Collection.SORT_ARTIST_ALPHA; + default: + return Collection.SORT_NOT; + } + } + } + + private void showAlbumFancyDropDown() { + if (mAlbum != null) { + CollectionManager.get().getAvailableCollections(mAlbum).done( + new DoneCallback>() { + @Override + public void onDone(final List result) { + int initialSelection = 0; + for (int i = 0; i < result.size(); i++) { + if (result.get(i) == mCollection) { + initialSelection = i; + break; + } + } + showFancyDropDown(initialSelection, mAlbum.getPrettyName(), + FancyDropDown.convertToDropDownItemInfo(result), + new FancyDropDown.DropDownListener() { + @Override + public void onDropDownItemSelected(int position) { + mCollection = result.get(position); + updateAdapter(); + } + + @Override + public void onCancel() { + } + }); + } + }); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistsFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistsFragment.java new file mode 100644 index 000000000..351902b08 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PlaylistsFragment.java @@ -0,0 +1,214 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.ListItemString; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.dialogs.CreatePlaylistDialog; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * {@link TomahawkFragment} which shows a set of {@link org.tomahawk.libtomahawk.collection.Playlist}s + * inside its {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ +public class PlaylistsFragment extends TomahawkFragment { + + private final HashSet mResolvingUsers = new HashSet<>(); + + @SuppressWarnings("unused") + public void onEventAsync(DatabaseHelper.PlaylistsUpdatedEvent event) { + scheduleUpdateAdapter(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.playlistsfragment_layout, container, false); + } + + @Override + public void onResume() { + super.onResume(); + + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (mUser == user) { + CollectionManager.get().fetchPlaylists(); + } else { + mHideRemoveButton = true; + } + } + }); + + if (mContainerFragmentClass == null) { + getActivity().setTitle(getString(R.string.drawer_title_playlists).toUpperCase()); + if (getView() != null) { + View newButton = getView().findViewById(R.id.create_new_button); + newButton.setVisibility(View.VISIBLE); + newButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showCreateDialog(); + } + }); + } + } + updateAdapter(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (item instanceof Playlist) { + String playlistId = ((Playlist) item).getId(); + if (mQueryArray != null) { + ArrayList entries = new ArrayList<>(); + for (Query query : mQueryArray) { + entries.add(PlaylistEntry.get(playlistId, query, + IdGenerator.getLifetimeUniqueStringId())); + } + CollectionManager.get().addPlaylistEntries(playlistId, entries); + // invalidate the current list of entries + ((Playlist) item).setFilled(false); + } + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.PLAYLIST, ((Playlist) item).getCacheKey()); + if (mUser != null) { + bundle.putString(TomahawkFragment.USER, mUser.getId()); + } + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), + PlaylistEntriesFragment.class, bundle); + } + getArguments().remove(QUERYARRAY); + mQueryArray = null; + } + + public void showCreateDialog() { + ArrayList queries = mQueryArray != null ? mQueryArray : new ArrayList(); + Playlist playlist = Playlist.fromQueryList( + IdGenerator.getLifetimeUniqueStringId(), "", null, queries); + CreatePlaylistDialog dialog = new CreatePlaylistDialog(); + Bundle args = new Bundle(); + args.putString(TomahawkFragment.PLAYLIST, playlist.getCacheKey()); + args.putString(TomahawkFragment.USER, mUser.getCacheKey()); + dialog.setArguments(args); + dialog.show(getFragmentManager(), null); + } + + /** + * Called every time an item inside a ListView or GridView is long-clicked + * + * @param item the Object which corresponds to the long-click + * @param segment + */ + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + return FragmentUtils.showContextMenu((TomahawkMainActivity) getActivity(), item, null, + false, mHideRemoveButton); + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + final List segments = new ArrayList<>(); + + if (mQueryArray != null) { + // Add the header text item + List textItems = new ArrayList(); + textItems.add(new ListItemString( + getResources().getQuantityString(R.plurals.add_to_playlist_headertext, + mQueryArray.size(), mQueryArray.size()), true)); + segments.add(new Segment.Builder(textItems).build()); + } + + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + List playlists = new ArrayList(); + if (mUser.getPlaylists() == null) { + if (mUser != user && !mResolvingUsers.contains(mUser)) { + String requestId = InfoSystem.get().resolvePlaylists(mUser, false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + mResolvingUsers.add(mUser); + } + } else { + playlists.addAll(mUser.getPlaylists()); + } + segments.add(new Segment.Builder(playlists) + .showAsGrid(R.integer.grid_column_count, R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + fillAdapter(segments); + showContentHeader(R.drawable.playlists_header); + if (getView() != null) { + View newButton = getView().findViewById(R.id.create_new_button); + int y = mHeaderNonscrollableHeight + - getResources().getDimensionPixelSize( + R.dimen.row_height_medium) + - getResources().getDimensionPixelSize(R.dimen.padding_small); + newButton.setY(y); + } + } + }); + } + }); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceAdvancedFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceAdvancedFragment.java new file mode 100755 index 000000000..01e2c0587 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceAdvancedFragment.java @@ -0,0 +1,166 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.FakePreferencesAdapter; +import org.tomahawk.tomahawk_android.utils.FakePreferenceGroup; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link TomahawkListFragment} which fakes the standard {@link android.preference.PreferenceFragment} + * behaviour. We need to fake it, because the official support library doesn't provide a {@link + * android.preference.PreferenceFragment} class + */ +public class PreferenceAdvancedFragment extends TomahawkListFragment + implements OnItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + + public static final String PREFERENCE_ID_PREFBITRATE = "pref_bitrate"; + + public static final String PREFERENCE_ID_PLUGINTOPLAY = "plugin_to_play"; + + public static final String PREFERENCE_ID_SCROBBLEEVERYTHING = "scrobble_everything"; + + public static final String PREFERENCE_ID_EQUALIZER = "mEqualizerValues"; + + /** + * Called, when this {@link PreferenceAdvancedFragment}'s {@link android.view.View} has been + * created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + PreferenceManager.getDefaultSharedPreferences(getActivity()) + .registerOnSharedPreferenceChangeListener(this); + + // Set up the set of FakePreferences to be shown in this Fragment + List fakePreferenceGroups = new ArrayList<>(); + FakePreferenceGroup prefGroup = new FakePreferenceGroup(); + + FakePreferenceGroup.FakePreference pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_EQUALIZER; + pref.title = getString(R.string.preferences_equalizer); + pref.summary = getString(R.string.preferences_equalizer_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_CHECKBOX; + pref.id = PREFERENCE_ID_PLUGINTOPLAY; + pref.storageKey = PreferenceUtils.PLUG_IN_TO_PLAY; + pref.title = getString(R.string.preferences_plug_and_play); + pref.summary = getString(R.string.preferences_plug_and_play_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + pref.type = FakePreferenceGroup.TYPE_PLAIN; + } else { + pref.type = FakePreferenceGroup.TYPE_CHECKBOX; + } + pref.id = PREFERENCE_ID_SCROBBLEEVERYTHING; + pref.storageKey = PreferenceUtils.SCROBBLE_EVERYTHING; + pref.title = getString(R.string.preferences_playback_data); + pref.summary = getString(R.string.preferences_playback_data_text, + HatchetAuthenticatorUtils.HATCHET_PRETTY_NAME); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_SPINNER; + pref.id = PREFERENCE_ID_PREFBITRATE; + pref.storageKey = PreferenceUtils.PREF_BITRATE; + pref.title = getString(R.string.preferences_audio_quality); + pref.summary = getString(R.string.preferences_audio_quality_text); + prefGroup.addFakePreference(pref); + + fakePreferenceGroups.add(prefGroup); + + // Now we can push the complete set of FakePreferences into our FakePreferencesAdapter, + // so that it can provide our ListView with the correct Views. + FakePreferencesAdapter fakePreferencesAdapter = new FakePreferencesAdapter( + getActivity().getLayoutInflater(), fakePreferenceGroups); + setListAdapter(fakePreferencesAdapter); + + getListView().setOnItemClickListener(this); + setupNonScrollableSpacer(getListView()); + } + + /** + * Initialize + */ + @Override + public void onResume() { + super.onResume(); + + getListAdapter().notifyDataSetChanged(); + } + + /** + * Called every time an item inside the {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + * is clicked + * + * @param parent The AdapterView where the click happened. + * @param view The view within the AdapterView that was clicked (this will be a view + * provided by the adapter) + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + FakePreferenceGroup.FakePreference fakePreference + = (FakePreferenceGroup.FakePreference) getListAdapter().getItem(position); + if (fakePreference.type == FakePreferenceGroup.TYPE_CHECKBOX) { + // if a FakePreference of type "TYPE_CHECKBOX" has been clicked, + // we edit the associated SharedPreference and toggle its boolean value + boolean preferenceState = PreferenceUtils.getBoolean(fakePreference.storageKey); + PreferenceUtils.edit() + .putBoolean(fakePreference.storageKey, !preferenceState) + .commit(); + } else if (fakePreference.type == FakePreferenceGroup.TYPE_PLAIN) { + if (fakePreference.id.equals(PREFERENCE_ID_EQUALIZER)) { + Bundle bundle = new Bundle(); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_ACTIONBAR_FILLED); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), EqualizerFragment.class, + bundle); + } else if (fakePreference.id.equals(PREFERENCE_ID_SCROBBLEEVERYTHING)) { + PreferenceUtils.askAccess(getActivity()); + } + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + getListAdapter().notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceConnectFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceConnectFragment.java new file mode 100755 index 000000000..62155e83b --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceConnectFragment.java @@ -0,0 +1,202 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.ListItemString; +import org.tomahawk.libtomahawk.resolver.HatchetStubResolver; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.libtomahawk.resolver.UserCollectionStubResolver; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.dialogs.ConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.DirectoryChooserConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.GMusicConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.HatchetLoginDialog; +import org.tomahawk.tomahawk_android.dialogs.ResolverConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.ResolverRedirectConfigDialog; +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * {@link org.tomahawk.tomahawk_android.fragments.TomahawkListFragment} which fakes the standard + * {@link android.preference.PreferenceFragment} behaviour. We need to fake it, because the official + * support library doesn't provide a {@link android.preference.PreferenceFragment} class + */ +public class PreferenceConnectFragment extends TomahawkListFragment + implements MultiColumnClickListener { + + private static final String TAG = PreferenceConnectFragment.class.getSimpleName(); + + @SuppressWarnings("unused") + public void onEventMainThread(CollectionManager.UpdatedEvent event) { + getListAdapter().notifyDataSetChanged(); + } + + @SuppressWarnings("unused") + public void onEventMainThread(AuthenticatorManager.ConfigTestResultEvent event) { + getListAdapter().notifyDataSetChanged(); + } + + @SuppressWarnings("unused") + public void onEventMainThread(ScriptResolver.EnabledStateChangedEvent event) { + getListAdapter().notifyDataSetChanged(); + } + + @SuppressWarnings("unused") + public void onEventMainThread(PipeLine.ResolversChangedEvent event) { + updateAdapter(); + } + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.PreferenceConnectFragment}'s + * {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + updateAdapter(); + } + + private void updateAdapter() { + List segments = new ArrayList<>(); + + // Add the header text item + List textItems = new ArrayList<>(); + textItems.add(new ListItemString(getString(R.string.connect_headertext))); + Segment segment = new Segment.Builder(textItems).build(); + segments.add(segment); + + // Add all resolver grid items + List resolvers = new ArrayList<>(); + resolvers.add(UserCollectionStubResolver.get()); + if (mContainerFragmentClass == null + || !mContainerFragmentClass.equals(WelcomeFragment.class.getName())) { + resolvers.add(HatchetStubResolver.get()); + } + List scriptResolvers = PipeLine.get().getScriptResolvers(); + Collections.sort(scriptResolvers, new Comparator() { + @Override + public int compare(ScriptResolver lhs, ScriptResolver rhs) { + return lhs.getPrettyName().compareToIgnoreCase(rhs.getPrettyName()); + } + }); + for (ScriptResolver scriptResolver : scriptResolvers) { + //TODO: Remove this hack once we can get rid of Tomahawk.resolver.instance completely (see ScriptAccount#onWebViewClientReady) + if (!scriptResolver.getId().contains("-metadata") + && !scriptResolver.getId().equals("echonest") + && !scriptResolver.getId().equals("itunes") + && !scriptResolver.getScriptAccount().isManuallyInstalled()) { + resolvers.add(scriptResolver); + } + } + segment = new Segment.Builder(resolvers) + .showAsGrid(R.integer.grid_column_count, R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + segments.add(segment); + + resolvers = new ArrayList<>(); + for (ScriptResolver scriptResolver : scriptResolvers) { + if (!scriptResolver.getId().contains("-metadata") + && scriptResolver.getScriptAccount().isManuallyInstalled()) { + resolvers.add(scriptResolver); + } + } + segment = new Segment.Builder(resolvers) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.connect_header_manualresolvers) + .showAsGrid(R.integer.grid_column_count, R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .build(); + segments.add(segment); + + if (getListView() != null) { + if (getListAdapter() == null) { + TomahawkListAdapter tomahawkListAdapter = new TomahawkListAdapter( + (TomahawkMainActivity) getActivity(), getActivity().getLayoutInflater(), + segments, getListView(), this); + setListAdapter(tomahawkListAdapter); + } else { + ((TomahawkListAdapter) getListAdapter()).setSegments(segments, getListView()); + } + + setupNonScrollableSpacer(getListView()); + } else { + Log.d(TAG, "Couldn't update adapter because getListView() returned null!"); + } + } + + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (item instanceof Resolver) { + String id = ((Resolver) item).getId(); + ConfigDialog dialog; + switch (id) { + case TomahawkApp.PLUGINNAME_SPOTIFY: + case TomahawkApp.PLUGINNAME_DEEZER: + dialog = new ResolverRedirectConfigDialog(); + break; + case TomahawkApp.PLUGINNAME_USERCOLLECTION: + dialog = new DirectoryChooserConfigDialog(); + break; + case TomahawkApp.PLUGINNAME_HATCHET: + dialog = new HatchetLoginDialog(); + break; + case TomahawkApp.PLUGINNAME_GMUSIC: + AccountManager accountManager = AccountManager.get(TomahawkApp.getContext()); + Account[] accounts = accountManager.getAccountsByType("com.google"); + if (accounts != null && accounts.length > 0) { + dialog = new GMusicConfigDialog(); + } else { + dialog = new ResolverConfigDialog(); + } + break; + default: + dialog = new ResolverConfigDialog(); + break; + } + Bundle args = new Bundle(); + args.putString(TomahawkFragment.PREFERENCEID, id); + dialog.setArguments(args); + dialog.show(getFragmentManager(), null); + } + } + + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + return false; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceInfoFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceInfoFragment.java new file mode 100755 index 000000000..c7e7afc47 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferenceInfoFragment.java @@ -0,0 +1,178 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import com.uservoice.uservoicesdk.UserVoice; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.adapters.FakePreferencesAdapter; +import org.tomahawk.tomahawk_android.dialogs.ConfigDialog; +import org.tomahawk.tomahawk_android.dialogs.SendLogConfigDialog; +import org.tomahawk.tomahawk_android.utils.FakePreferenceGroup; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link org.tomahawk.tomahawk_android.fragments.TomahawkListFragment} which fakes the standard + * {@link android.preference.PreferenceFragment} behaviour. We need to fake it, because the official + * support library doesn't provide a {@link android.preference.PreferenceFragment} class + */ +public class PreferenceInfoFragment extends TomahawkListFragment + implements OnItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + + private static final String TAG = PreferenceInfoFragment.class.getSimpleName(); + + public static final String PREFERENCE_ID_APPVERSION = "app_version"; + + public static final String PREFERENCE_ID_USERVOICE = "uservoice"; + + public static final String PREFERENCE_ID_SENDLOG = "sendlog"; + + public static final String PREFERENCE_ID_PLAYSTORELINK = "playstore_link"; + + public static final String PREFERENCE_ID_WEBSITELINK = "website_link"; + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.PreferenceInfoFragment}'s + * {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + PreferenceManager.getDefaultSharedPreferences(getActivity()) + .registerOnSharedPreferenceChangeListener(this); + + // Set up the set of FakePreferences to be shown in this Fragment + List fakePreferenceGroups = new ArrayList<>(); + FakePreferenceGroup prefGroup = new FakePreferenceGroup(); + + FakePreferenceGroup.FakePreference pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_USERVOICE; + pref.title = getString(R.string.preferences_app_uservoice); + pref.summary = getString(R.string.preferences_app_uservoice_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_PLAYSTORELINK; + pref.title = getString(R.string.preferences_app_playstore_link); + pref.summary = getString(R.string.preferences_app_playstore_link_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_WEBSITELINK; + pref.title = getString(R.string.preferences_app_website_link); + pref.summary = getString(R.string.preferences_app_website_link_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_SENDLOG; + pref.title = getString(R.string.preferences_app_sendlog); + pref.summary = getString(R.string.preferences_app_sendlog_text); + prefGroup.addFakePreference(pref); + + pref = new FakePreferenceGroup.FakePreference(); + pref.type = FakePreferenceGroup.TYPE_PLAIN; + pref.id = PREFERENCE_ID_APPVERSION; + pref.title = getString(R.string.preferences_app_version); + pref.summary = ""; + try { + if (getActivity().getPackageManager() != null) { + PackageInfo packageInfo = getActivity().getPackageManager() + .getPackageInfo(getActivity().getPackageName(), 0); + pref.summary = packageInfo.versionName; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "onViewCreated: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + prefGroup.addFakePreference(pref); + + fakePreferenceGroups.add(prefGroup); + + // Now we can push the complete set of FakePreferences into our FakePreferencesAdapter, + // so that it can provide our ListView with the correct Views. + FakePreferencesAdapter fakePreferencesAdapter = new FakePreferencesAdapter( + getActivity().getLayoutInflater(), fakePreferenceGroups); + setListAdapter(fakePreferencesAdapter); + + getListView().setOnItemClickListener(this); + setupNonScrollableSpacer(getListView()); + } + + /** + * Initialize + */ + @Override + public void onResume() { + super.onResume(); + + getListAdapter().notifyDataSetChanged(); + } + + /** + * Called every time an item inside the {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + * is clicked + * + * @param parent The AdapterView where the click happened. + * @param view The view within the AdapterView that was clicked (this will be a view + * provided by the adapter) + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + FakePreferenceGroup.FakePreference fakePreference + = (FakePreferenceGroup.FakePreference) getListAdapter().getItem(position); + if (fakePreference.id.equals(PREFERENCE_ID_USERVOICE)) { + UserVoice.launchUserVoice(getActivity()); + } else if (fakePreference.id.equals(PREFERENCE_ID_SENDLOG)) { + ConfigDialog dialog = new SendLogConfigDialog(); + dialog.show(getFragmentManager(), null); + } else if (fakePreference.id.equals(PREFERENCE_ID_PLAYSTORELINK)) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("market://details?id=org.tomahawk.tomahawk_android")); + startActivity(i); + } else if (fakePreference.id.equals(PREFERENCE_ID_WEBSITELINK)) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://www.tomahawk-player.org/")); + startActivity(i); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + getListAdapter().notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferencePagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferencePagerFragment.java new file mode 100644 index 000000000..234f8125a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/PreferencePagerFragment.java @@ -0,0 +1,78 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; + +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class PreferencePagerFragment extends PagerFragment { + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.PreferencePagerFragment}'s + * {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getActivity().setTitle(getString(R.string.drawer_title_settings).toUpperCase()); + + int initialPage = -1; + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + initialPage = getArguments().getInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + } + + showContentHeader(R.drawable.settings_header); + + List fragmentInfoLists = new ArrayList<>(); + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PreferenceConnectFragment.class; + fragmentInfo.mTitle = getString(R.string.connect); + fragmentInfo.mIconResId = R.drawable.ic_connect; + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PreferenceAdvancedFragment.class; + fragmentInfo.mTitle = getString(R.string.advanced); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PreferenceInfoFragment.class; + fragmentInfo.mTitle = getString(R.string.info); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + setupPager(fragmentInfoLists, initialPage, null, 1); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SearchPagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SearchPagerFragment.java new file mode 100644 index 000000000..1d1f7732d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SearchPagerFragment.java @@ -0,0 +1,297 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.infosystem.hatchet.Search; +import org.tomahawk.libtomahawk.infosystem.hatchet.SearchResult; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.ThreadManager; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class SearchPagerFragment extends PagerFragment { + + private String mCurrentQueryString; + + protected final Set mCorrespondingQueries + = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final ArrayList mAlbumIds = new ArrayList<>(); + + private final ArrayList mArtistIds = new ArrayList<>(); + + private final ArrayList mUserIds = new ArrayList<>(); + + private Playlist mTrackResultPlaylist = + Playlist.fromEmptyList(IdGenerator.getSessionUniqueStringId(), ""); + + private SearchFragmentReceiver mSearchFragmentReceiver; + + private boolean mIsFirstBroadcast; + + /** + * Handles incoming broadcasts. + */ + private class SearchFragmentReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + boolean noConnectivity = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + if (!noConnectivity && !mIsFirstBroadcast) { + resolveFullTextQuery(mCurrentQueryString); + } + mIsFirstBroadcast = false; + } + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(PipeLine.ResultsEvent event) { + if (mCorrespondingQueries.contains(event.mQuery)) { + mTrackResultPlaylist = event.mQuery.getResultPlaylist(); + updatePager(); + } + } + + /** + * Restore the {@link String} inside the search {@link android.widget.TextView}. Either through + * the savedInstanceState {@link Bundle} or through the a {@link Bundle} provided in the + * Arguments. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null + && savedInstanceState.containsKey(TomahawkFragment.QUERY_STRING) + && savedInstanceState.getString(TomahawkFragment.QUERY_STRING) != null) { + mCurrentQueryString = savedInstanceState.getString( + TomahawkFragment.QUERY_STRING); + } + } + + + /** + * Called, when this {@link SearchPagerFragment}'s {@link android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + int initialPage = -1; + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + initialPage = getArguments().getInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + if (getArguments().containsKey(TomahawkFragment.QUERY_STRING) + && getArguments().getString(TomahawkFragment.QUERY_STRING) != null) { + mCurrentQueryString = getArguments().getString( + TomahawkFragment.QUERY_STRING); + } + } + + // If we have restored a CurrentQueryString, start searching, so that we show the proper + // results again + if (mCurrentQueryString != null) { + resolveFullTextQuery(mCurrentQueryString); + getActivity().setTitle(mCurrentQueryString); + } + + showContentHeader(null); + + updatePager(initialPage); + } + + @Override + public void onStart() { + super.onStart(); + + // Initialize and register Receiver + if (mSearchFragmentReceiver == null) { + mIsFirstBroadcast = true; + mSearchFragmentReceiver = new SearchFragmentReceiver(); + IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + getActivity().registerReceiver(mSearchFragmentReceiver, intentFilter); + } + } + + @Override + public void onPause() { + super.onPause(); + + for (Query query : mCorrespondingQueries) { + if (ThreadManager.get().stop(query)) { + mCorrespondingQueries.remove(query); + } + } + } + + @Override + public void onStop() { + super.onStop(); + + if (mSearchFragmentReceiver != null) { + getActivity().unregisterReceiver(mSearchFragmentReceiver); + mSearchFragmentReceiver = null; + } + } + + /** + * Save the {@link String} inside the search {@link android.widget.TextView}. + */ + @Override + public void onSaveInstanceState(Bundle out) { + out.putString(TomahawkFragment.QUERY_STRING, mCurrentQueryString); + super.onSaveInstanceState(out); + } + + private void updatePager() { + updatePager(-1); + } + + private void updatePager(int initialPage) { + List fragmentInfoLists = new ArrayList<>(); + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PlaylistEntriesFragment.class; + fragmentInfo.mTitle = getString(R.string.songs); + fragmentInfo.mBundle = getChildFragmentBundle(); + if (mTrackResultPlaylist != null) { + fragmentInfo.mBundle.putString( + TomahawkFragment.PLAYLIST, mTrackResultPlaylist.getCacheKey()); + } + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = ArtistsFragment.class; + fragmentInfo.mTitle = getString(R.string.artists); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putStringArrayList(TomahawkFragment.ARTISTARRAY, mArtistIds); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = AlbumsFragment.class; + fragmentInfo.mTitle = getString(R.string.albums); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putStringArrayList(TomahawkFragment.ALBUMARRAY, mAlbumIds); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = UsersFragment.class; + fragmentInfo.mTitle = getString(R.string.users); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putStringArrayList(TomahawkFragment.USERARRAY, mUserIds); + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + setupPager(fragmentInfoLists, initialPage, null, 1); + } + + /** + * Invoke the resolving process with the given fullTextQuery {@link String} + */ + public void resolveFullTextQuery(String fullTextQuery) { + ((TomahawkMainActivity) getActivity()).closeDrawer(); + mTrackResultPlaylist = Playlist.fromEmptyList(IdGenerator.getSessionUniqueStringId(), ""); + mAlbumIds.clear(); + mArtistIds.clear(); + mUserIds.clear(); + mCurrentQueryString = fullTextQuery; + mCorrespondingRequestIds.clear(); + String requestId = InfoSystem.get().resolve(fullTextQuery); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + Query query = PipeLine.get().resolve(fullTextQuery, false); + if (query != null) { + mCorrespondingQueries.clear(); + mCorrespondingQueries.add(query); + } + } + + @Override + protected void onInfoSystemResultsReported(InfoRequestData infoRequestData) { + if (mCorrespondingRequestIds.contains(infoRequestData.getRequestId())) { + List results = infoRequestData.getResultList(Search.class); + if (results != null && results.size() > 0) { + Search search = results.get(0); + float maxScore = 0f; + Image contentHeaderImage = null; + for (SearchResult result : search.getSearchResults()) { + Object resultObject = result.getResult(); + if (resultObject instanceof Artist) { + Artist artist = (Artist) resultObject; + mArtistIds.add(artist.getCacheKey()); + if (artist.getImage() != null && result.getScore() > maxScore) { + maxScore = result.getScore(); + contentHeaderImage = artist.getImage(); + } + } else if (resultObject instanceof Album) { + Album album = (Album) resultObject; + mAlbumIds.add(album.getCacheKey()); + if (album.getImage() != null && result.getScore() > maxScore) { + maxScore = result.getScore(); + contentHeaderImage = album.getImage(); + } + } else if (resultObject instanceof User) { + User user = (User) resultObject; + mUserIds.add(user.getCacheKey()); + if (user.getImage() != null && result.getScore() > maxScore) { + maxScore = result.getScore(); + contentHeaderImage = user.getImage(); + } + } + } + showContentHeader(contentHeaderImage); + } + updatePager(); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SocialActionsFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SocialActionsFragment.java new file mode 100644 index 000000000..c7ce5c342 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/SocialActionsFragment.java @@ -0,0 +1,368 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.infosystem.hatchet.HatchetInfoPlugin; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; + +import android.os.Bundle; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; +import android.view.View; +import android.widget.AbsListView; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +/** + * {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment} which shows information provided + * by a User object. Such as the image, feed and nowPlaying info of a user. + */ +public class SocialActionsFragment extends TomahawkFragment implements + StickyListHeadersListView.OnHeaderClickListener { + + private static final String TAG = SocialActionsFragment.class.getSimpleName(); + + public static final int SHOW_MODE_SOCIALACTIONS = 0; + + public static final int SHOW_MODE_DASHBOARD = 1; + + public final HashSet mResolvingPages = new HashSet<>(); + + private List mSuggestedUsers; + + private String mRandomUsersRequestId; + + @SuppressWarnings("unused") + public void onEvent(InfoSystem.ResultsEvent event) { + if (mRandomUsersRequestId != null + && mRandomUsersRequestId.equals(event.mInfoRequestData.getRequestId())) { + mSuggestedUsers = event.mInfoRequestData.getResultList(User.class); + } + + super.onEvent(event); + } + + @Override + public void onResume() { + super.onResume(); + if (mUser == null) { + return; + } + + mHideRemoveButton = true; + + if (mShowMode == SHOW_MODE_DASHBOARD) { + if (mContainerFragmentClass == null) { + getActivity().setTitle(getString(R.string.drawer_title_feed).toUpperCase()); + } + for (Date date : mUser.getFriendsFeed().keySet()) { + String requestId = InfoSystem.get().resolveFriendsFeed(mUser, date); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (user.getFollowCount() >= 0 && user.getFollowCount() <= 5 + && mRandomUsersRequestId == null) { + mRandomUsersRequestId = InfoSystem.get().getRandomUsers(5); + mCorrespondingRequestIds.add(mRandomUsersRequestId); + } + } + }); + } else { + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + for (Date date : mUser.getSocialActions().keySet()) { + String requestId = InfoSystem.get().resolveSocialActions(mUser, date); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + getListView().setOnHeaderClickListener(this); + updateAdapter(); + } + + @Override + public void onPause() { + super.onPause(); + + mResolvingPages.clear(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * + * @param view the clicked view + * @param o the Object which corresponds to the click + */ + @Override + public void onItemClick(View view, Object o, Segment segment) { + if (getMediaController() == null) { + Log.e(TAG, "onItemClick failed because getMediaController() is null"); + return; + } + final TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + + Object item = o; + if (o instanceof SocialAction) { + item = ((SocialAction) o).getTargetObject(); + } + Bundle bundle = new Bundle(); + if (item instanceof User) { + bundle.putString(TomahawkFragment.USER, ((User) item).getId()); + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_USER); + FragmentUtils.replace(activity, UserPagerFragment.class, bundle); + } else if (item instanceof Query && o instanceof SocialAction) { + PlaylistEntry entry = mUser.getPlaylistEntry((SocialAction) o); + if (entry.getQuery().isPlayable()) { + if (getPlaybackManager().getCurrentEntry() == entry) { + // if the user clicked on an already playing track + int playState = getMediaController().getPlaybackState().getState(); + if (playState == PlaybackStateCompat.STATE_PLAYING) { + getMediaController().getTransportControls().pause(); + } else if (playState == PlaybackStateCompat.STATE_PAUSED) { + getMediaController().getTransportControls().play(); + } + } else { + Playlist playlist; + if (mShowMode == SHOW_MODE_SOCIALACTIONS) { + playlist = mUser.getSocialActionsPlaylist(); + } else { + playlist = mUser.getFriendsFeedPlaylist(); + } + getPlaybackManager().setPlaylist(playlist, entry); + getMediaController().getTransportControls().play(); + } + } + } else if (item instanceof Album) { + bundle.putString(TomahawkFragment.ALBUM, ((Album) item).getCacheKey()); + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + FragmentUtils.replace(activity, PlaylistEntriesFragment.class, bundle); + } else if (item instanceof Artist) { + bundle.putString(TomahawkFragment.ARTIST, ((Artist) item).getCacheKey()); + bundle.putString(TomahawkFragment.COLLECTION_ID, mCollection.getId()); + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC_PAGER); + bundle.putLong(CONTAINER_FRAGMENT_ID, + IdGenerator.getSessionUniqueId()); + FragmentUtils.replace(activity, ArtistPagerFragment.class, bundle); + } else if (item instanceof Playlist) { + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + bundle.putString(TomahawkFragment.PLAYLIST, ((Playlist) item).getCacheKey()); + FragmentUtils.replace(activity, PlaylistEntriesFragment.class, bundle); + } + } + + @Override + public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, + long headerId, boolean currentlySticky) { + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + Object item = getListAdapter().getItem(itemPosition); + if (item instanceof List && !((List) item).isEmpty()) { + item = ((List) item).get(0); + } + if (item instanceof SocialAction) { + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.USER, + ((SocialAction) item).getUser().getId()); + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_USER); + FragmentUtils.replace(activity, UserPagerFragment.class, bundle); + } + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + if (mShowMode == SHOW_MODE_DASHBOARD && !getResources().getBoolean(R.bool.is_landscape)) { + setAreHeadersSticky(true); + } + TomahawkRunnable r = new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_VERYHIGH) { + @Override + public void run() { + if (mUser != null) { + List segments = new ArrayList<>(); + int extraPadding = TomahawkApp.getContext().getResources() + .getDimensionPixelSize(R.dimen.padding_medium) + + ViewUtils.convertDpToPixel(32); + if (mSuggestedUsers != null) { + List suggestions = new ArrayList<>(); + suggestions.addAll(mSuggestedUsers); + Segment segment = new Segment.Builder(suggestions) + .headerLayout(R.layout.list_header_socialaction_fake) + .headerString(TomahawkApp.getContext().getString( + R.string.suggest_users) + ":") + .leftExtraPadding(extraPadding) + .build(); + segments.add(segment); + } + TreeMap> socialActionsList; + if (mShowMode == SHOW_MODE_DASHBOARD) { + socialActionsList = mUser.getFriendsFeed(); + } else { + socialActionsList = mUser.getSocialActions(); + } + List> mergedActions = mergeSocialActions(socialActionsList); + for (List actions : mergedActions) { + Segment segment = toSegment(actions); + segments.add(segment); + } + fillAdapter(segments); + } + } + }; + ThreadManager.get().execute(r); + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + super.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + + if (mUser != null && firstVisibleItem + visibleItemCount + 5 > totalItemCount) { + mShowMode = getArguments().getInt(SHOW_MODE); + if (mShowMode == SHOW_MODE_DASHBOARD) { + if (!mResolvingPages.contains(mUser.getFriendsFeedNextDate())) { + mResolvingPages.add(mUser.getFriendsFeedNextDate()); + String requestId = InfoSystem.get() + .resolveFriendsFeed(mUser, mUser.getFriendsFeedNextDate()); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } else { + if (!mResolvingPages.contains(mUser.getSocialActionsNextDate())) { + mResolvingPages.add(mUser.getSocialActionsNextDate()); + String requestId = InfoSystem.get() + .resolveSocialActions(mUser, mUser.getSocialActionsNextDate()); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + } + } + + public static List> mergeSocialActions( + TreeMap> actions) { + List> mergedActionsList = new ArrayList<>(); + for (List socialActions : actions.descendingMap().values()) { + mergedActionsList.addAll(mergeSocialActions(socialActions)); + } + return mergedActionsList; + } + + public static List> mergeSocialActions(List actions) { + List> mergedActionsList = new ArrayList<>(); + Set checkedActions = new HashSet<>(); + for (SocialAction socialAction : actions) { + if (!checkedActions.contains(socialAction) + && shouldDisplayAction(socialAction)) { + List mergedActions = new ArrayList<>(); + mergedActions.add(socialAction); + checkedActions.add(socialAction); + for (SocialAction actionToCompare : actions) { + if (!checkedActions.contains(actionToCompare) + && shouldMergeAction(actionToCompare, socialAction)) { + mergedActions.add(actionToCompare); + checkedActions.add(actionToCompare); + } + } + mergedActionsList.add(mergedActions); + } + } + return mergedActionsList; + } + + private static boolean shouldDisplayAction(SocialAction socialAction) { + boolean action = Boolean.valueOf(socialAction.getAction()); + String type = socialAction.getType(); + return HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_CREATEPLAYLIST.equals(type) + || HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_LATCHON.equals(type) + || HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_FOLLOW.equals(type) + || (action && HatchetInfoPlugin.HATCHET_SOCIALACTION_TYPE_LOVE.equals(type)); + } + + private static boolean shouldMergeAction(SocialAction actionToCompare, + SocialAction socialAction) { + return actionToCompare.getUser() == socialAction.getUser() + && actionToCompare.getType().equals(socialAction.getType()) + && actionToCompare.getTargetObject().getClass() + == socialAction.getTargetObject().getClass(); + } + + private Segment toSegment(List actions) { + SocialAction first = actions.get(0); + Segment.Builder builder; + if (first.getTargetObject() instanceof Album + || first.getTargetObject() instanceof User + || first.getTargetObject() instanceof Artist + || first.getTargetObject() instanceof Playlist) { + builder = new Segment.Builder(actions) + .headerLayout(R.layout.list_header_socialaction) + .showAsGrid(R.integer.grid_column_count_feed, + R.dimen.padding_superlarge, R.dimen.padding_small); + } else { + builder = new Segment.Builder(actions) + .headerLayout(R.layout.list_header_socialaction); + } + int extraPadding = TomahawkApp.getContext().getResources() + .getDimensionPixelSize(R.dimen.padding_medium) + + ViewUtils.convertDpToPixel(32); + builder.leftExtraPadding(extraPadding); + return builder.build(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/StationsFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/StationsFragment.java new file mode 100644 index 000000000..3c3b4880b --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/StationsFragment.java @@ -0,0 +1,184 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.ListItemDrawable; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.dialogs.CreateStationDialog; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +public class StationsFragment extends TomahawkFragment { + + private static final String TAG = StationsFragment.class.getSimpleName(); + + @SuppressWarnings("unused") + public void onEventAsync(DatabaseHelper.PlaylistsUpdatedEvent event) { + scheduleUpdateAdapter(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.playlistsfragment_layout, container, false); + } + + @Override + public void onResume() { + super.onResume(); + + if (mContainerFragmentClass == null) { + getActivity().setTitle(getString(R.string.drawer_title_stations).toUpperCase()); + if (getView() != null) { + View newButton = getView().findViewById(R.id.create_new_button); + newButton.setVisibility(View.VISIBLE); + newButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showCreateDialog(); + } + }); + } + } + updateAdapter(); + } + + public void showCreateDialog() { + CreateStationDialog dialog = new CreateStationDialog(); + dialog.show(getFragmentManager(), null); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * + * @param view the clicked view + * @param item the Object which corresponds to the click + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (getMediaController() == null) { + Log.e(TAG, "onItemClick failed because getMediaController() is null"); + return; + } + if (item instanceof StationPlaylist) { + if (item != getPlaybackManager().getPlaylist()) { + getPlaybackManager().setPlaylist((StationPlaylist) item); + getMediaController().getTransportControls().play(); + } + } else if (item instanceof ListItemDrawable) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("http://the.echonest.com/")); + startActivity(i); + } + } + + /** + * Called every time an item inside a ListView or GridView is long-clicked + * + * @param item the Object which corresponds to the long-click + */ + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + String myStationsString = getString(R.string.my_stations); + return segment != null && segment.getHeaderString() != null + && segment.getHeaderString().equals(myStationsString) + && FragmentUtils.showContextMenu((TomahawkMainActivity) getActivity(), item, null, + false, mHideRemoveButton); + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + final List segments = new ArrayList<>(); + List myStations = new ArrayList(); + myStations.addAll(DatabaseHelper.get().getStations()); + segments.add(new Segment.Builder(myStations) + .showAsGrid(R.integer.grid_column_count, R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.my_stations) + .build()); + segments.add(new Segment.Builder(getSuggestedStations()) + .showAsGrid(R.integer.grid_column_count, R.dimen.padding_superlarge, + R.dimen.padding_superlarge) + .headerLayout(R.layout.single_line_list_header) + .headerString(R.string.suggested_stations) + .build()); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + fillAdapter(segments); + showContentHeader(R.drawable.stations_header); + if (getView() != null) { + View newButton = getView().findViewById(R.id.create_new_button); + int y = mHeaderNonscrollableHeight + - getResources().getDimensionPixelSize( + R.dimen.row_height_medium) + - getResources().getDimensionPixelSize(R.dimen.padding_small); + newButton.setY(y); + } + } + }); + } + }); + } + + private static List getSuggestedStations() { + List suggestedStations = new ArrayList(); + String[] suggestedGenres = new String[]{"acoustic", "alternative", "club", "dance", + "drum-and-bass", "dubstep", "electronic", "funk", "goth", "grunge", "happy", + "hard-rock", "heavy-metal", "hip-hop", "house", "indie", "j-pop", "jazz", "k-pop", + "metal", "party", "pop", "punk-rock", "r-n-b", "reggae", "rock-n-roll", "romance", + "singer-songwriter", "soul", "techno", "trance", "trip-hop", "world-music"}; + for (String suggestedGenre : suggestedGenres) { + ArrayList genres = new ArrayList<>(); + genres.add(suggestedGenre); + suggestedStations.add(StationPlaylist.get(null, null, genres)); + } + return suggestedStations; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkFragment.java new file mode 100644 index 000000000..39a0a322c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkFragment.java @@ -0,0 +1,787 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter; +import org.tomahawk.tomahawk_android.listeners.MultiColumnClickListener; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.utils.ProgressBarUpdater; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.tomahawk.tomahawk_android.utils.TomahawkRunnable; +import org.tomahawk.tomahawk_android.utils.WeakReferenceHandler; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.util.Pair; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AbsListView; +import android.widget.AdapterView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +/** + * The base class for every {@link android.support.v4.app.Fragment} that displays a collection + * object + */ +public abstract class TomahawkFragment extends TomahawkListFragment + implements MultiColumnClickListener, AbsListView.OnScrollListener { + + private static final String TAG = TomahawkFragment.class.getSimpleName(); + + public static final String ALBUM = "album"; + + public static final String ALBUMARRAY = "albumarray"; + + public static final String ARTIST = "artist"; + + public static final String ARTISTARRAY = "artistarray"; + + public static final String PLAYLIST = "playlist"; + + public static final String STATION = "station"; + + public static final String USER = "user"; + + public static final String USERARRAY = "userarray"; + + public static final String SOCIALACTION = "socialaction"; + + public static final String PLAYLISTENTRY = "playlistentry"; + + public static final String QUERY = "query"; + + public static final String QUERYARRAY = "queryarray"; + + public static final String PREFERENCEID = "preferenceid"; + + public static final String TOMAHAWKLISTITEM = "tomahawklistitem"; + + public static final String TOMAHAWKLISTITEM_TYPE = "tomahawklistitem_type"; + + public static final String FROM_PLAYBACKFRAGMENT = "from_playbackfragment"; + + public static final String HIDE_REMOVE_BUTTON = "hide_remove_button"; + + public static final String QUERY_STRING = "query_string"; + + public static final String SHOW_MODE = "show_mode"; + + public static final String CONTAINER_FRAGMENT_CLASSNAME = "container_fragment_classname"; + + public static final String LIST_SCROLL_POSITION = "list_scroll_position"; + + public static final String MESSAGE = "message"; + + protected static final int RESOLVE_QUERIES_REPORTER_MSG = 1336; + + protected static final long RESOLVE_QUERIES_REPORTER_DELAY = 100; + + protected static final int ADAPTER_UPDATE_MSG = 1337; + + protected static final long ADAPTER_UPDATE_DELAY = 500; + + private TomahawkListAdapter mTomahawkListAdapter; + + private ProgressBarUpdater mProgressBarUpdater = new ProgressBarUpdater( + new ProgressBarUpdater.UpdateProgressRunnable() { + @Override + public void updateProgress(PlaybackStateCompat playbackState, long duration) { + if (playbackState != null && mTomahawkListAdapter != null + && mTomahawkListAdapter.getProgressBar() != null) { + long currentPosition = playbackState.getPosition(); + if (playbackState.getState() != PlaybackStateCompat.STATE_PAUSED) { + // Calculate the elapsed time between the last position update and now + // and unless paused, we can assume (delta * speed) + current position + // is approximately the latest position. This ensure that we do not + // repeatedly call the getPlaybackState() on MediaControllerCompat. + long timeDelta = SystemClock.elapsedRealtime() - + playbackState.getLastPositionUpdateTime(); + currentPosition += (int) timeDelta * playbackState.getPlaybackSpeed(); + } + mTomahawkListAdapter.getProgressBar().setProgress( + (int) ((float) currentPosition / duration + * mTomahawkListAdapter.getProgressBar().getMax())); + } + } + }); + + protected boolean mIsResumed; + + protected final Set mCorrespondingRequestIds = + Collections.newSetFromMap(new ConcurrentHashMap()); + + protected final HashSet mResolvingItems = new HashSet<>(); + + protected final Set mCorrespondingQueries = + Collections.newSetFromMap(new ConcurrentHashMap()); + + protected ArrayList mQueryArray; + + protected ArrayList mAlbumArray; + + protected ArrayList mArtistArray; + + protected ArrayList mUserArray; + + protected Album mAlbum; + + protected Artist mArtist; + + protected Playlist mPlaylist; + + protected User mUser; + + protected Query mQuery; + + private int mFirstVisibleItemLastTime = -1; + + private int mVisibleItemCount = 0; + + protected int mShowMode = -1; + + private final MediaControllerCompat.Callback mCallback = new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) { + Log.d(TAG, "onPlaybackstate changed" + state); + if (mTomahawkListAdapter != null) { + boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING; + mTomahawkListAdapter.setHighlightedItemIsPlaying(isPlaying); + mTomahawkListAdapter.notifyDataSetChanged(); + mProgressBarUpdater.setPlaybackState(state); + if (isPlaying) { + mProgressBarUpdater.scheduleSeekbarUpdate(); + } else { + mProgressBarUpdater.stopSeekbarUpdate(); + } + } + TomahawkFragment.this.onPlaybackStateChanged(state); + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + Log.d(TAG, "onMetadataChanged changed" + metadata); + if (mTomahawkListAdapter != null && metadata != null) { + if (getPlaybackManager().getCurrentEntry() != null) { + mProgressBarUpdater.setCurrentDuration( + metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)); + mTomahawkListAdapter + .setHighlightedEntry(getPlaybackManager().getCurrentEntry()); + mTomahawkListAdapter + .setHighlightedQuery(getPlaybackManager().getCurrentQuery()); + mTomahawkListAdapter.notifyDataSetChanged(); + } + } + TomahawkFragment.this.onMetadataChanged(metadata); + } + + @Override + public void onQueueChanged(List queue) { + Log.d(TAG, "onQueueChanged changed queue.size()= " + queue.size()); + TomahawkFragment.this.onQueueChanged(queue); + } + }; + + private final Handler mResolveQueriesHandler = new ResolveQueriesHandler(this); + + private static class ResolveQueriesHandler extends WeakReferenceHandler { + + public ResolveQueriesHandler(TomahawkFragment referencedObject) { + super(referencedObject); + } + + @Override + public void handleMessage(Message msg) { + TomahawkFragment fragment = getReferencedObject(); + if (fragment != null && getReferencedObject().shouldAutoResolve()) { + Log.d(TAG, "Auto resolving ..."); + removeMessages(msg.what); + getReferencedObject().resolveItemsFromTo( + getReferencedObject().mFirstVisibleItemLastTime - 2, + getReferencedObject().mFirstVisibleItemLastTime + + getReferencedObject().mVisibleItemCount + 2); + } + } + } + + // Handler which reports the PipeLine's and InfoSystem's results in intervals + private final Handler mAdapterUpdateHandler = new AdapterUpdateHandler(this); + + private static class AdapterUpdateHandler extends WeakReferenceHandler { + + public AdapterUpdateHandler(TomahawkFragment referencedObject) { + super(referencedObject); + } + + @Override + public void handleMessage(Message msg) { + TomahawkFragment fragment = getReferencedObject(); + if (fragment != null) { + removeMessages(msg.what); + fragment.updateAdapter(); + } + } + } + + @SuppressWarnings("unused") + public void onEvent(PipeLine.ResolversChangedEvent event) { + forceResolveVisibleItems(event.mManuallyAdded); + } + + @SuppressWarnings("unused") + public void onEvent(PipeLine.ResultsEvent event) { + if (mCorrespondingQueries.contains(event.mQuery)) { + scheduleUpdateAdapter(); + } + } + + @SuppressWarnings("unused") + public void onEvent(InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + scheduleUpdateAdapter(); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(CollectionManager.UpdatedEvent event) { + if (event.mUpdatedItemIds != null) { + if ((mPlaylist != null && event.mUpdatedItemIds.contains(mPlaylist.getId())) + || (mAlbum != null && event.mUpdatedItemIds.contains(mAlbum.getCacheKey())) + || (mArtist != null && event.mUpdatedItemIds.contains(mArtist.getCacheKey())) + || (mQuery != null && event.mUpdatedItemIds.contains(mQuery.getCacheKey()))) { + scheduleUpdateAdapter(); + } + } else { + scheduleUpdateAdapter(); + } + } + + @Override + public void onResume() { + super.onResume(); + + if (getArguments() != null) { + if (getArguments().containsKey(ALBUM) + && !TextUtils.isEmpty(getArguments().getString(ALBUM))) { + mAlbum = Album.getByKey(getArguments().getString(ALBUM)); + if (mAlbum == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else { + String requestId = InfoSystem.get().resolve(mAlbum); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + if (getArguments().containsKey(PLAYLIST) + && !TextUtils.isEmpty(getArguments().getString(PLAYLIST))) { + mPlaylist = Playlist.getByKey(getArguments().getString(TomahawkFragment.PLAYLIST)); + if (mPlaylist == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (mUser != user) { + String requestId = InfoSystem.get().resolve(mPlaylist); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + }); + } + } + if (getArguments().containsKey(ARTIST) + && !TextUtils.isEmpty(getArguments().getString(ARTIST))) { + mArtist = Artist.getByKey(getArguments().getString(ARTIST)); + if (mArtist == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else { + String requestId = InfoSystem.get().resolve(mArtist, true); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + if (getArguments().containsKey(USER) + && !TextUtils.isEmpty(getArguments().getString(USER))) { + mUser = User.getUserById(getArguments().getString(USER)); + if (mUser == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else if (mUser.getName() == null) { + String requestId = InfoSystem.get().resolve(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + if (getArguments().containsKey(QUERY) + && !TextUtils.isEmpty(getArguments().getString(QUERY))) { + mQuery = Query.getByKey(getArguments().getString(QUERY)); + if (mQuery == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else { + String requestId = InfoSystem.get().resolve(mQuery.getArtist(), false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + ArrayList argList = getArguments().getStringArrayList(USERARRAY); + if (argList != null) { + mUserArray = new ArrayList<>(); + for (String userId : argList) { + mUserArray.add(User.getUserById(userId)); + } + } + argList = getArguments().getStringArrayList(ARTISTARRAY); + if (argList != null) { + mArtistArray = new ArrayList<>(); + for (String artistKey : argList) { + Artist artist = Artist.getByKey(artistKey); + if (artist != null) { + mArtistArray.add(artist); + } + } + } + argList = getArguments().getStringArrayList(ALBUMARRAY); + if (argList != null) { + mAlbumArray = new ArrayList<>(); + for (String albumKey : argList) { + Album album = Album.getByKey(albumKey); + if (album != null) { + mAlbumArray.add(album); + } + } + } + argList = getArguments().getStringArrayList(QUERYARRAY); + if (argList != null) { + mQueryArray = new ArrayList<>(); + for (String queryKey : argList) { + Query query = Query.getByKey(queryKey); + if (query != null) { + mQueryArray.add(query); + } + } + } + if (getArguments().containsKey(SHOW_MODE)) { + mShowMode = getArguments().getInt(SHOW_MODE); + } + } + + StickyListHeadersListView list = getListView(); + if (list != null) { + list.setOnScrollListener(this); + } + + mIsResumed = true; + } + + @Override + public void onPause() { + super.onPause(); + + for (Query query : mCorrespondingQueries) { + if (ThreadManager.get().stop(query)) { + mCorrespondingQueries.remove(query); + } + } + + mAdapterUpdateHandler.removeCallbacksAndMessages(null); + + mIsResumed = false; + + if (mTomahawkListAdapter != null) { + mTomahawkListAdapter.closeSegments(null); + mTomahawkListAdapter = null; + } + + mProgressBarUpdater.stopSeekbarUpdate(); + } + + @Override + public void onStart() { + super.onStart(); + Log.d(TAG, "onStart()"); + onMediaControllerConnected(); + } + + @Override + public void onStop() { + super.onStop(); + Log.d(TAG, "onStop()"); + if (getMediaController() != null) { + getMediaController().unregisterCallback(mCallback); + } + } + + @Override + public void onMediaControllerConnected() { + super.onMediaControllerConnected(); + Log.d(TAG, "onMediaControllerConnected()"); + if (getMediaController() != null) { + onPlaybackStateChanged(getMediaController().getPlaybackState()); + onMetadataChanged(getMediaController().getMetadata()); + getMediaController().registerCallback(mCallback); + } else { + Log.e(TAG, "Couldn't get MediaController object!"); + } + } + + @Override + public abstract void onItemClick(View view, Object item, Segment segment); + + /** + * Called every time an item inside a ListView or GridView is long-clicked + * + * @param item the Object which corresponds to the long-click + * @param segment + */ + @Override + public boolean onItemLongClick(View view, Object item, Segment segment) { + return FragmentUtils.showContextMenu((TomahawkMainActivity) getActivity(), item, + mCollection.getId(), false, mHideRemoveButton); + } + + protected void fillAdapter(Segment segment, Collection collection) { + List segments = new ArrayList<>(); + segments.add(segment); + fillAdapter(segments, null, collection); + } + + protected void fillAdapter(Segment segment) { + List segments = new ArrayList<>(); + segments.add(segment); + fillAdapter(segments, null, null); + } + + protected void fillAdapter(List segments) { + fillAdapter(segments, null, null); + } + + protected void fillAdapter(List segments, Collection collection) { + fillAdapter(segments, null, collection); + } + + protected void fillAdapter(final List segments, final View headerSpacerForwardView, + final Collection collection) { + final TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (activity != null && getListView() != null) { + if (mTomahawkListAdapter == null) { + LayoutInflater inflater = activity.getLayoutInflater(); + TomahawkListAdapter adapter = new TomahawkListAdapter(activity, + inflater, + segments, collection, getListView(), TomahawkFragment.this); + TomahawkFragment.super.setListAdapter(adapter); + mTomahawkListAdapter = adapter; + } else { + mTomahawkListAdapter.setSegments(segments, getListView()); + } + forceResolveVisibleItems(false); + setupNonScrollableSpacer(getListView()); + setupScrollableSpacer(getListAdapter(), getListView(), + headerSpacerForwardView); + if (headerSpacerForwardView == null) { + setupAnimations(); + } + } else { + Log.e(TAG, "fillAdapter - getActivity() or getListView() returned null!"); + } + } + }); + } + + /** + * Get the {@link TomahawkListAdapter} associated with this activity's ListView. + */ + public TomahawkListAdapter getListAdapter() { + return (TomahawkListAdapter) super.getListAdapter(); + } + + protected void setAreHeadersSticky(final boolean areHeadersSticky) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (getListView() != null) { + getListView().setAreHeadersSticky(areHeadersSticky); + } else { + Log.e(TAG, "setAreHeadersSticky - getListView() returned null!"); + } + } + }); + } + + protected void scheduleUpdateAdapter() { + if (!mAdapterUpdateHandler.hasMessages(ADAPTER_UPDATE_MSG)) { + mAdapterUpdateHandler.sendEmptyMessageDelayed(ADAPTER_UPDATE_MSG, + ADAPTER_UPDATE_DELAY); + } + } + + /** + * Update this {@link TomahawkFragment}'s {@link TomahawkListAdapter} content + */ + protected abstract void updateAdapter(); + + protected void onPlaybackStateChanged(PlaybackStateCompat playbackState) { + } + + protected void onMetadataChanged(MediaMetadataCompat metadata) { + } + + protected void onQueueChanged(List queue) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + super.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + + mVisibleItemCount = visibleItemCount; + if (mFirstVisibleItemLastTime != firstVisibleItem) { + mFirstVisibleItemLastTime = firstVisibleItem; + mResolveQueriesHandler.removeCallbacksAndMessages(null); + mResolveQueriesHandler.sendEmptyMessageDelayed(RESOLVE_QUERIES_REPORTER_MSG, + RESOLVE_QUERIES_REPORTER_DELAY); + } + } + + protected void forceResolveVisibleItems(boolean reresolve) { + if (reresolve) { + mCorrespondingQueries.clear(); + } + mResolveQueriesHandler.removeCallbacksAndMessages(null); + mResolveQueriesHandler.sendEmptyMessageDelayed(RESOLVE_QUERIES_REPORTER_MSG, + RESOLVE_QUERIES_REPORTER_DELAY); + } + + private void resolveItemsFromTo(int start, int end) { + if (mTomahawkListAdapter != null) { + start = Math.max(start, 0); + end = Math.min(end, mTomahawkListAdapter.getCount()); + for (int i = start; i < end; i++) { + Object object = mTomahawkListAdapter.getItem(i); + if (object instanceof List) { + for (Object item : (List) object) { + resolveItem(item); + } + } else { + resolveItem(object); + } + } + } + } + + private void resolveItem(final Object object) { + if (object instanceof PlaylistEntry || object instanceof Query) { + Query query; + if (object instanceof PlaylistEntry) { + PlaylistEntry entry = (PlaylistEntry) object; + query = entry.getQuery(); + } else { + query = (Query) object; + } + if (!mCorrespondingQueries.contains(query)) { + mCorrespondingQueries.add(PipeLine.get().resolve(query)); + } + } else if (object instanceof StationPlaylist) { + resolveItem((StationPlaylist) object); + } else if (object instanceof Playlist) { + resolveItem((Playlist) object); + } else if (object instanceof SocialAction) { + resolveItem((SocialAction) object); + } else if (object instanceof Album) { + resolveItem((Album) object); + } else if (object instanceof Artist) { + resolveItem((Artist) object); + } else if (object instanceof User) { + resolveItem((User) object); + } + } + + private void resolveItem(StationPlaylist stationPlaylist) { + if (mResolvingItems.add(stationPlaylist)) { + if (stationPlaylist.getArtists() != null) { + for (Pair pair : stationPlaylist.getArtists()) { + resolveItem(pair.first); + } + } + if (stationPlaylist.getTracks() != null) { + for (Pair pair : stationPlaylist.getTracks()) { + resolveItem(pair.first.getArtist()); + } + } + } + } + + private void resolveItem(final Playlist playlist) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (mUser == null || mUser == user) { + TomahawkRunnable r = new TomahawkRunnable( + TomahawkRunnable.PRIORITY_IS_DATABASEACTION) { + @Override + public void run() { + if (mResolvingItems.add(playlist)) { + Playlist pl = playlist; + if (pl.size() == 0) { + pl = DatabaseHelper.get().getPlaylist(pl.getId()); + } + if (pl != null && pl.size() > 0) { + boolean isFavorites = mUser != null + && pl == mUser.getFavorites(); + pl.updateTopArtistNames(isFavorites); + DatabaseHelper.get().updatePlaylist(pl); + if (pl.getTopArtistNames() != null) { + for (int i = 0; i < pl.getTopArtistNames().length && i < 5; + i++) { + resolveItem(Artist.get(pl.getTopArtistNames()[i])); + } + } + } else { + mResolvingItems.remove(pl); + } + } + } + }; + ThreadManager.get().execute(r); + } + } + }); + } + + private void resolveItem(SocialAction socialAction) { + if (mResolvingItems.add(socialAction)) { + if (socialAction.getTargetObject() != null) { + resolveItem(socialAction.getTargetObject()); + } + resolveItem(socialAction.getUser()); + } + } + + private void resolveItem(Album album) { + if (mResolvingItems.add(album)) { + if (album.getImage() == null) { + String requestId = InfoSystem.get().resolve(album); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + resolveItem(album.getArtist()); + } + + private void resolveItem(Artist artist) { + if (mResolvingItems.add(artist)) { + if (artist.getImage() == null) { + String requestId = InfoSystem.get().resolve(artist, false); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + } + + private void resolveItem(User user) { + if (mResolvingItems.add(user)) { + if (user.getImage() == null) { + String requestId = InfoSystem.get().resolve(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + } + + protected AdapterView.OnItemSelectedListener constructDropdownListener(final String prefKey) { + return new AdapterView.OnItemSelectedListener() { + @SuppressLint("CommitPrefEdits") + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (getDropdownPos(prefKey) != position) { + PreferenceUtils.edit().putInt(prefKey, position).commit(); + updateAdapter(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }; + } + + protected int getDropdownPos(String prefKey) { + return PreferenceUtils.getInt(prefKey, 0); + } + + private boolean shouldAutoResolve() { + return mContainerFragmentClass == null + || !mContainerFragmentClass.equals(SearchPagerFragment.class.getName()) + && (mCollection == null + || mCollection.getId().equals(TomahawkApp.PLUGINNAME_HATCHET) + || mCollection.getId().equals(TomahawkApp.PLUGINNAME_USERCOLLECTION)); + } +} + diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkListFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkListFragment.java new file mode 100644 index 000000000..47471f161 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/TomahawkListFragment.java @@ -0,0 +1,273 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.adapters.StickyBaseAdapter; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; + +import de.greenrobot.event.EventBus; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +/** + * More customizable implementation of {@link android.app.ListFragment} + */ +public abstract class TomahawkListFragment extends ContentHeaderFragment implements + AbsListView.OnScrollListener { + + private static final String TAG = TomahawkListFragment.class.getSimpleName(); + + private StickyBaseAdapter mStickyBaseAdapter; + + private StickyListHeadersListView mList; + + private Parcelable mListState = null; + + private boolean restoreScrollPosition = true; + + private final Handler mHandler = new Handler(); + + private final Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + protected String mContainerFragmentClass; + + /** + * Get a stored list scroll position, if present + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(TomahawkFragment.LIST_SCROLL_POSITION)) { + mListState = savedInstanceState.getParcelable( + TomahawkFragment.LIST_SCROLL_POSITION); + } + } + + if (getArguments() != null) { + mContainerFragmentClass = + getArguments().getString(TomahawkFragment.CONTAINER_FRAGMENT_CLASSNAME); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.tomahawklistfragment_layout, container, false); + } + + @Override + public void onResume() { + super.onResume(); + + if (getListView() != null) { + getListView().setOnScrollListener(this); + getListView().setAreHeadersSticky(getResources().getBoolean(R.bool.set_headers_sticky)); + if (mContainerFragmentPage > -1) { + getListView().setTag(mContainerFragmentPage); + } + } + } + + @Override + public void onPause() { + super.onPause(); + + mListState = getListState(); + } + + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + super.onDestroyView(); + } + + @Override + public void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + + out.putParcelable(TomahawkFragment.LIST_SCROLL_POSITION, mListState); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + updateAnimation(); + } + + private void updateAnimation() { + int playTime; + if (getListView() != null && getListView().getFirstVisiblePosition() == 0 + && getListView().getListChildAt(0) != null) { + FrameLayout.LayoutParams params = + (FrameLayout.LayoutParams) getListView().getLayoutParams(); + float delta = getListView().getListChildAt(0).getBottom() - getListView().getTop() + + params.topMargin; + playTime = (int) + (10000f - delta / getListView().getListChildAt(0).getHeight() * 10000f); + } else { + playTime = 10000; + } + if (mContainerFragmentId >= 0) { + AnimateEvent event = new AnimateEvent(); + event.mContainerFragmentId = mContainerFragmentId; + event.mContainerFragmentPage = mContainerFragmentPage; + event.mPlayTime = playTime; + EventBus.getDefault().post(event); + } else { + animate(playTime); + } + } + + @SuppressWarnings("unused") + public void onEvent(RequestSyncEvent event) { + if (mContainerFragmentId == event.mContainerFragmentId + && mContainerFragmentPage == event.mPerformerFragmentPage + && getListView() != null) { + PerformSyncEvent performSyncEvent = new PerformSyncEvent(); + performSyncEvent.mContainerFragmentId = event.mContainerFragmentId; + performSyncEvent.mContainerFragmentPage = event.mReceiverFragmentPage; + performSyncEvent.mFirstVisiblePosition = getListView().getFirstVisiblePosition(); + performSyncEvent.mTop = getListView().getListChildAt(0).getTop(); + EventBus.getDefault().post(performSyncEvent); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(PerformSyncEvent event) { + if (mContainerFragmentId == event.mContainerFragmentId + && mContainerFragmentPage == event.mContainerFragmentPage + && getListView() != null) { + if (event.mFirstVisiblePosition == 0) { + getListView().setSelectionFromTop(0, event.mTop); + } else if (getListView().getFirstVisiblePosition() == 0) { + getListView().setSelection(1); + } + } + } + + protected void showContentHeader(Object item) { + if (mContainerFragmentClass == null) { + super.showContentHeader(item); + } + } + + protected void setupAnimations() { + if (mContainerFragmentClass == null) { + super.setupAnimations(); + } + } + + /** + * Get this {@link TomahawkListFragment}'s {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ + public StickyListHeadersListView getListView() { + ensureList(); + return mList; + } + + /** + * Set mList/mGrid to the listview/gridview layout element and catch possible exceptions. + */ + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + LayoutInflater layoutInflater = (LayoutInflater) + TomahawkApp.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + if (root == null) { + Log.e(TAG, "Couldn't inflate listview! root is null"); + return; + } + if (layoutInflater == null) { + Log.e(TAG, "Couldn't inflate listview! layoutInflater is null"); + return; + } + View view = ViewUtils.ensureInflation(root, R.id.listview_stub, R.id.listview); + if (view instanceof StickyListHeadersListView) { + mList = (StickyListHeadersListView) view; + } + if (mList == null) { + Log.e(TAG, "Something went wrong, listview is null"); + } + if (mStickyBaseAdapter != null) { + setListAdapter(mStickyBaseAdapter); + } + mHandler.post(mRequestFocus); + } + + /** + * @return the current scrolling position of the list- or gridView + */ + private Parcelable getListState() { + if (getListView() != null) { + return getListView().getWrappedList().onSaveInstanceState(); + } + return null; + } + + /** + * Get the {@link BaseAdapter} associated with this activity's ListView. + */ + public StickyBaseAdapter getListAdapter() { + return mStickyBaseAdapter; + } + + /** + * Set the {@link BaseAdapter} associated with this activity's ListView. + */ + public void setListAdapter(StickyBaseAdapter adapter) { + mStickyBaseAdapter = adapter; + if (getListView() != null) { + getListView().setAdapter(adapter); + if (restoreScrollPosition && mListState != null) { + getListView().getWrappedList().onRestoreInstanceState(mListState); + } + } + } + + public void setRestoreScrollPosition(boolean restoreScrollPosition) { + this.restoreScrollPosition = restoreScrollPosition; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UserPagerFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UserPagerFragment.java new file mode 100644 index 000000000..7ef5f14d7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UserPagerFragment.java @@ -0,0 +1,211 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.infosystem.InfoRequestData; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class UserPagerFragment extends PagerFragment { + + private User mUser; + + /** + * Called, when this {@link org.tomahawk.tomahawk_android.fragments.UserPagerFragment}'s {@link + * android.view.View} has been created + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getActivity().setTitle(""); + + int initialPage = -1; + if (getArguments() != null) { + if (getArguments().containsKey(TomahawkFragment.CONTAINER_FRAGMENT_PAGE)) { + initialPage = getArguments().getInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE); + } + if (getArguments().containsKey(TomahawkFragment.USER) && !TextUtils + .isEmpty(getArguments().getString(TomahawkFragment.USER))) { + mUser = User.getUserById(getArguments().getString(TomahawkFragment.USER)); + if (mUser == null) { + getActivity().getSupportFragmentManager().popBackStack(); + return; + } else if (mUser.getName() == null) { + String requestId = InfoSystem.get().resolve(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + } + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (user != null && user.getFollowings() == null) { + String requestId = InfoSystem.get().resolveFollowings(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + } + }); + + mFollowButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + final HatchetAuthenticatorUtils authUtils = + (HatchetAuthenticatorUtils) AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + if (user.getFollowings() != null + && user.getFollowings().containsKey(mUser)) { + String relationshipId = user.getFollowings().get(mUser); + InfoSystem.get().deleteRelationship(authUtils, relationshipId); + mShowFakeNotFollowing = true; + mShowFakeFollowing = false; + } else { + InfoSystem.get().sendRelationshipPostStruct(authUtils, mUser); + mShowFakeNotFollowing = false; + mShowFakeFollowing = true; + } + } + }); + showContentHeader(mUser); + } + }; + + showContentHeader(mUser); + + List fragmentInfoLists = new ArrayList<>(); + FragmentInfoList fragmentInfoList = new FragmentInfoList(); + FragmentInfo fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = SocialActionsFragment.class; + fragmentInfo.mTitle = getString(R.string.activity); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mBundle + .putInt(TomahawkFragment.SHOW_MODE, SocialActionsFragment.SHOW_MODE_SOCIALACTIONS); + fragmentInfo.mIconResId = R.drawable.ic_action_activity; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = AlbumsFragment.class; + fragmentInfo.mTitle = getString(R.string.drawer_title_collection); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_collection; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PlaylistsFragment.class; + fragmentInfo.mTitle = getString(R.string.drawer_title_playlists); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_playlist; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PlaylistEntriesFragment.class; + fragmentInfo.mTitle = getString(R.string.history); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putInt(TomahawkFragment.SHOW_MODE, + PlaylistEntriesFragment.SHOW_MODE_PLAYBACKLOG); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_history; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = PlaylistEntriesFragment.class; + fragmentInfo.mTitle = getString(R.string.drawer_title_lovedtracks); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putInt(TomahawkFragment.SHOW_MODE, + PlaylistEntriesFragment.SHOW_MODE_LOVEDITEMS); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_favorites; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoList.setCurrent( + PreferenceUtils.getInt(PreferenceUtils.USERPAGER_SELECTOR_POSITION)); + fragmentInfoLists.add(fragmentInfoList); + + fragmentInfoList = new FragmentInfoList(); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = UsersFragment.class; + fragmentInfo.mTitle = getString(R.string.followers); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putInt(TomahawkFragment.SHOW_MODE, + UsersFragment.SHOW_MODE_TYPE_FOLLOWERS); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_friend; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfo = new FragmentInfo(); + fragmentInfo.mClass = UsersFragment.class; + fragmentInfo.mTitle = getString(R.string.following); + fragmentInfo.mBundle = getChildFragmentBundle(); + fragmentInfo.mBundle.putInt(TomahawkFragment.SHOW_MODE, + UsersFragment.SHOW_MODE_TYPE_FOLLOWINGS); + fragmentInfo.mBundle.putString(TomahawkFragment.USER, mUser.getCacheKey()); + fragmentInfo.mIconResId = R.drawable.ic_action_friend; + fragmentInfoList.addFragmentInfo(fragmentInfo); + fragmentInfoLists.add(fragmentInfoList); + + setupPager(fragmentInfoLists, initialPage, PreferenceUtils.USERPAGER_SELECTOR_POSITION, 1); + } + + @Override + protected void onInfoSystemResultsReported(InfoRequestData infoRequestData) { + InfoRequestData sentLoggedOp = InfoSystem.get() + .getSentLoggedOpById(infoRequestData.getRequestId()); + if (sentLoggedOp != null + && sentLoggedOp.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_RELATIONSHIPS + && (sentLoggedOp.getHttpType() == InfoRequestData.HTTPTYPE_DELETE + || sentLoggedOp.getHttpType() == InfoRequestData.HTTPTYPE_POST)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + String requestId = InfoSystem.get().resolveFollowings(user); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + }); + } + if (mCorrespondingRequestIds.contains(infoRequestData.getRequestId())) { + if (infoRequestData.getType() == InfoRequestData.INFOREQUESTDATA_TYPE_USERS_FOLLOWS) { + mShowFakeFollowing = false; + mShowFakeNotFollowing = false; + } + } + showContentHeader(mUser); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UsersFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UsersFragment.java new file mode 100644 index 000000000..1ff7cd436 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/UsersFragment.java @@ -0,0 +1,105 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.Segment; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; + +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment} which shows a set of {@link + * org.tomahawk.libtomahawk.collection.Artist}s inside its {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} + */ +public class UsersFragment extends TomahawkFragment { + + public static final int SHOW_MODE_TYPE_FOLLOWINGS = 0; + + public static final int SHOW_MODE_TYPE_FOLLOWERS = 1; + + @Override + public void onResume() { + super.onResume(); + + if (mShowMode == SHOW_MODE_TYPE_FOLLOWERS) { + String requestId = InfoSystem.get().resolveFollowers(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } else { + String requestId = InfoSystem.get().resolveFollowings(mUser); + if (requestId != null) { + mCorrespondingRequestIds.add(requestId); + } + } + if (mContainerFragmentClass == null) { + getActivity().setTitle(""); + } + updateAdapter(); + } + + /** + * Called every time an item inside a ListView or GridView is clicked + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment + */ + @Override + public void onItemClick(View view, Object item, Segment segment) { + if (item instanceof User) { + Bundle bundle = new Bundle(); + bundle.putInt(TomahawkFragment.SHOW_MODE, + SocialActionsFragment.SHOW_MODE_SOCIALACTIONS); + bundle.putString(TomahawkFragment.USER, ((User) item).getId()); + bundle.putInt(CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_USER); + FragmentUtils.replace((TomahawkMainActivity) getActivity(), UserPagerFragment.class, + bundle); + } + } + + /** + * Update this {@link org.tomahawk.tomahawk_android.fragments.TomahawkFragment}'s {@link + * org.tomahawk.tomahawk_android.adapters.TomahawkListAdapter} content + */ + @Override + protected void updateAdapter() { + if (!mIsResumed) { + return; + } + + List users = new ArrayList(); + if (mShowMode == SHOW_MODE_TYPE_FOLLOWERS) { + if (mUser.getFollowers() != null) { + users.addAll(mUser.getFollowers().keySet()); + } + } else if (mUserArray != null) { + users.addAll(mUserArray); + } else if (mUser.getFollowings() != null) { + users.addAll(mUser.getFollowings().keySet()); + } + fillAdapter(new Segment.Builder(users).build()); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/fragments/WelcomeFragment.java b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/WelcomeFragment.java new file mode 100644 index 000000000..ab56a744a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/fragments/WelcomeFragment.java @@ -0,0 +1,202 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.fragments; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.HatchetLoginRegisterView; +import org.tomahawk.tomahawk_android.views.SimplePagerIndicator; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import de.greenrobot.event.EventBus; + +/** + * A {@link android.support.v4.app.Fragment} which is being shown to the user when he first opens + * the app + */ +public class WelcomeFragment extends Fragment { + + public final static String TAG = WelcomeFragment.class.getSimpleName(); + + private ViewPager mViewPager; + + private TextView mPositiveButton; + + private HatchetLoginRegisterView mHatchetLoginRegisterView; + + private class LoginRegisterPagerAdapter extends PagerAdapter { + + @Override + public int getCount() { + return 4; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + TomahawkMainActivity activity = (TomahawkMainActivity) getActivity(); + LayoutInflater inflater = activity.getLayoutInflater(); + View v = null; + switch (position) { + case 0: + v = inflater.inflate(R.layout.welcome_fragment_page_explanation, container, + false); + break; + case 1: + v = inflater.inflate(R.layout.welcome_fragment_page_setup, container, + false); + + Bundle args = new Bundle(); + args.putString(TomahawkFragment.CONTAINER_FRAGMENT_CLASSNAME, + WelcomeFragment.class.getName()); + args.putInt(TomahawkFragment.CONTAINER_FRAGMENT_PAGE, position); + args.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + TomahawkFragment.MODE_HEADER_NONE); + Fragment fragment = Fragment.instantiate( + activity, PreferenceConnectFragment.class.getName(), args); + FragmentTransaction ft = getChildFragmentManager().beginTransaction(); + ft.add(R.id.welcome_fragment_page_setup_container, fragment); + ft.commitAllowingStateLoss(); + break; + case 2: + v = inflater.inflate(R.layout.welcome_fragment_page_hatchet, container, + false); + ProgressBar progressBar = (ProgressBar) v.findViewById(R.id.smoothprogressbar); + HatchetAuthenticatorUtils authenticatorUtils = (HatchetAuthenticatorUtils) + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET); + mHatchetLoginRegisterView = + (HatchetLoginRegisterView) v.findViewById(R.id.hatchetloginregister); + mHatchetLoginRegisterView.setup(authenticatorUtils, progressBar); + break; + case 3: + v = inflater.inflate(R.layout.welcome_fragment_page_done, container, + false); + break; + } + if (v != null) { + container.addView(v); + } + return v; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + } + + private ViewPager.OnPageChangeListener mOnPageChangeListener + = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + int lastPage = mViewPager.getAdapter().getCount() - 1; + if (position == lastPage) { + mPositiveButton.setText(R.string.ok); + } else { + mPositiveButton.setText(R.string.next_page); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }; + + @SuppressWarnings("unused") + public void onEventMainThread(AuthenticatorManager.ConfigTestResultEvent event) { + if (mHatchetLoginRegisterView != null) { + mHatchetLoginRegisterView + .onConfigTestResult(event.mComponent, event.mType, event.mMessage); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRetainInstance(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.welcome_fragment, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mPositiveButton = (TextView) view.findViewById(R.id.config_dialog_positive_button); + mPositiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int lastPage = mViewPager.getAdapter().getCount() - 1; + if (mViewPager.getCurrentItem() == lastPage) { + PreferenceUtils.edit().putBoolean( + PreferenceUtils.COACHMARK_WELCOMEFRAGMENT_DISABLED, true).apply(); + getActivity().getSupportFragmentManager().popBackStack(); + } else { + mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1); + } + } + }); + mViewPager = (ViewPager) view.findViewById(R.id.viewpager); + mViewPager.setAdapter(new LoginRegisterPagerAdapter()); + SimplePagerIndicator indicator = + (SimplePagerIndicator) view.findViewById(R.id.simplepagerindicator); + indicator.setViewPager(mViewPager); + indicator.setOnPageChangeListener(mOnPageChangeListener); + } + + @Override + public void onStart() { + super.onStart(); + + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + + super.onStop(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MediaImageLoadedListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MediaImageLoadedListener.java new file mode 100644 index 000000000..9bbe17777 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MediaImageLoadedListener.java @@ -0,0 +1,23 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.listeners; + +public interface MediaImageLoadedListener { + + void onMediaImageLoaded(); +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MenuDrawerListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MenuDrawerListener.java new file mode 100644 index 000000000..246606de0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MenuDrawerListener.java @@ -0,0 +1,179 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.listeners; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.TomahawkMenuAdapter; +import org.tomahawk.tomahawk_android.fragments.ChartsSelectorFragment; +import org.tomahawk.tomahawk_android.fragments.CollectionPagerFragment; +import org.tomahawk.tomahawk_android.fragments.ContentHeaderFragment; +import org.tomahawk.tomahawk_android.fragments.PlaylistEntriesFragment; +import org.tomahawk.tomahawk_android.fragments.PlaylistsFragment; +import org.tomahawk.tomahawk_android.fragments.PreferencePagerFragment; +import org.tomahawk.tomahawk_android.fragments.SocialActionsFragment; +import org.tomahawk.tomahawk_android.fragments.StationsFragment; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.fragments.UserPagerFragment; +import org.tomahawk.tomahawk_android.utils.FragmentUtils; +import org.tomahawk.tomahawk_android.utils.MenuDrawer; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class MenuDrawerListener implements ListView.OnItemClickListener { + + private TomahawkMainActivity mActivity; + + private StickyListHeadersListView mDrawerList; + + private MenuDrawer mMenuDrawer; + + public MenuDrawerListener(TomahawkMainActivity activity, StickyListHeadersListView drawerList, + MenuDrawer menuDrawer) { + mActivity = activity; + mDrawerList = drawerList; + mMenuDrawer = menuDrawer; + } + + /** + * Called every time an item inside the {@link android.widget.ListView} is clicked + * + * @param parent The AdapterView where the click happened. + * @param view The view within the AdapterView that was clicked (this will be a view + * provided by the adapter) + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + TomahawkMenuAdapter.ResourceHolder holder = + (TomahawkMenuAdapter.ResourceHolder) mDrawerList.getAdapter().getItem(position); + final Bundle bundle = new Bundle(); + if (holder.collection != null) { + bundle.putString(TomahawkFragment.COLLECTION_ID, holder.collection.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + FragmentUtils.replace(mActivity, CollectionPagerFragment.class, bundle); + } else if (holder.id.equals(MenuDrawer.HUB_ID_USERPAGE)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_USER); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(mActivity, UserPagerFragment.class, bundle); + } + }); + } + }); + } else if (holder.id.equals(MenuDrawer.HUB_ID_FEED)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.SHOW_MODE, + SocialActionsFragment.SHOW_MODE_DASHBOARD); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_ACTIONBAR_FILLED); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(mActivity, SocialActionsFragment.class, bundle); + } + }); + } + }); + } else if (holder.id.equals(MenuDrawer.HUB_ID_CHARTS)) { + FragmentUtils + .replace(mActivity, ChartsSelectorFragment.class, bundle); + } else if (holder.id.equals(MenuDrawer.HUB_ID_COLLECTION)) { + bundle.putString(TomahawkFragment.COLLECTION_ID, + TomahawkApp.PLUGINNAME_USERCOLLECTION); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + FragmentUtils.replace(mActivity, CollectionPagerFragment.class, bundle); + } else if (holder.id.equals(MenuDrawer.HUB_ID_LOVEDTRACKS)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + bundle.putInt(TomahawkFragment.SHOW_MODE, + PlaylistEntriesFragment.SHOW_MODE_LOVEDITEMS); + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_DYNAMIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(mActivity, PlaylistEntriesFragment.class, bundle); + } + }); + } + }); + } else if (holder.id.equals(MenuDrawer.HUB_ID_PLAYLISTS)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(mActivity, PlaylistsFragment.class, bundle); + } + }); + } + }); + } else if (holder.id.equals(MenuDrawer.HUB_ID_STATIONS)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + bundle.putString(TomahawkFragment.USER, user.getId()); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + FragmentUtils.replace(mActivity, StationsFragment.class, bundle); + } + }); + } + }); + } else if (holder.id.equals(MenuDrawer.HUB_ID_SETTINGS)) { + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC_SMALL); + FragmentUtils.replace(mActivity, PreferencePagerFragment.class, bundle); + } + if (mMenuDrawer != null) { + mMenuDrawer.closeDrawer(); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MultiColumnClickListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MultiColumnClickListener.java new file mode 100644 index 000000000..c98167368 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/MultiColumnClickListener.java @@ -0,0 +1,43 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.listeners; + +import org.tomahawk.tomahawk_android.adapters.Segment; + +import android.view.View; + +public interface MultiColumnClickListener { + + /** + * Called every time an item inside a ListView or GridView is clicked + * + * @param view the clicked view + * @param item the Object which corresponds to the click + * @param segment the {@link Segment} which contains the clicked item + */ + void onItemClick(View view, Object item, Segment segment); + + /** + * Called every time an item inside a ListView or GridView is long-clicked + * + * @param view the clicked view + * @param item the Object which corresponds to the long-click + * @param segment the {@link Segment} which contains the clicked item + */ + boolean onItemLongClick(View view, Object item, Segment segment); +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/listeners/OnSizeChangedListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/OnSizeChangedListener.java new file mode 100644 index 000000000..476eeab1e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/OnSizeChangedListener.java @@ -0,0 +1,24 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.listeners; + +public interface OnSizeChangedListener { + + void onSizeChanged(int w, int h, int oldw, int oldh); + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/listeners/TomahawkPanelSlideListener.java b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/TomahawkPanelSlideListener.java new file mode 100644 index 000000000..c12e13fd6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/listeners/TomahawkPanelSlideListener.java @@ -0,0 +1,165 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.listeners; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.views.PlaybackPanel; + +import android.animation.Animator; +import android.view.View; + +import de.greenrobot.event.EventBus; + +public class TomahawkPanelSlideListener implements SlidingUpPanelLayout.PanelSlideListener { + + private TomahawkMainActivity mActivity; + + private float mSlidingOffset = -1f; + + private SlidingUpPanelLayout.PanelState mLastSlidingState; + + private SlidingUpPanelLayout mSlidingUpPanelLayout; + + private View mTopPanel; + + private PlaybackPanel mPlaybackPanel; + + public static class SlidingLayoutChangedEvent { + + public SlidingUpPanelLayout.PanelState mSlideState; + + } + + public TomahawkPanelSlideListener(TomahawkMainActivity activity, SlidingUpPanelLayout layout, + PlaybackPanel playbackPanel) { + mActivity = activity; + mSlidingUpPanelLayout = layout; + mPlaybackPanel = playbackPanel; + } + + @Override + public void onPanelSlide(View view, float v) { + mSlidingOffset = v; + mActivity.updateActionBarState(false); + if (mTopPanel == null) { + mTopPanel = mSlidingUpPanelLayout.findViewById(R.id.top_buttonpanel); + } + if (mTopPanel != null) { + if (v > 0.15f) { + AnimationUtils + .fade(mTopPanel, 0f, 1f, AnimationUtils.DURATION_PLAYBACKTOPPANEL, true, + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mTopPanel.findViewById(R.id.imageButton_repeat) + .setVisibility(View.VISIBLE); + mTopPanel.findViewById(R.id.close_button) + .setVisibility(View.VISIBLE); + mTopPanel.findViewById(R.id.imageButton_shuffle) + .setVisibility(View.VISIBLE); + animation.removeListener(this); + } + + @Override + public void onAnimationEnd(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } else if (v < 0.15f) { + AnimationUtils + .fade(mTopPanel, 1f, 0f, AnimationUtils.DURATION_PLAYBACKTOPPANEL, false, + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mTopPanel.findViewById(R.id.imageButton_repeat) + .setVisibility(View.GONE); + mTopPanel.findViewById(R.id.close_button) + .setVisibility(View.GONE); + mTopPanel.findViewById(R.id.imageButton_shuffle) + .setVisibility(View.GONE); + animation.removeListener(this); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } + } + int position = Math.min(10000, Math.max(0, (int) ((v - 0.8f) * 10000f / (1f - 0.8f)))); + mPlaybackPanel.animate(position); + sendSlidingLayoutChangedEvent(); + } + + @Override + public void onPanelCollapsed(View view) { + sendSlidingLayoutChangedEvent(); + } + + @Override + public void onPanelExpanded(View view) { + PreferenceUtils.edit().putBoolean( + PreferenceUtils.COACHMARK_PLAYBACKFRAGMENT_NAVIGATION_DISABLED, true) + .apply(); + sendSlidingLayoutChangedEvent(); + } + + @Override + public void onPanelAnchored(View view) { + sendSlidingLayoutChangedEvent(); + } + + @Override + public void onPanelHidden(View view) { + sendSlidingLayoutChangedEvent(); + } + + public float getSlidingOffset() { + return mSlidingOffset; + } + + private void sendSlidingLayoutChangedEvent() { + if (mSlidingUpPanelLayout.getPanelState() != mLastSlidingState) { + mLastSlidingState = mSlidingUpPanelLayout.getPanelState(); + + SlidingLayoutChangedEvent event = new SlidingLayoutChangedEvent(); + event.mSlideState = mSlidingUpPanelLayout.getPanelState(); + EventBus.getDefault().post(event); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/AndroidMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/AndroidMediaPlayer.java new file mode 100644 index 000000000..f5dc243f8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/AndroidMediaPlayer.java @@ -0,0 +1,209 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * Copyright 2016, Anton Romanov + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.utils.ThreadManager; + +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import java.io.IOException; + +public class AndroidMediaPlayer extends TomahawkMediaPlayer { + + private static final String TAG = AndroidMediaPlayer.class.getSimpleName(); + + private static MediaPlayer sMediaPlayer = null; + + private Query mPreparedQuery; + + private Query mPreparingQuery; + + private int mPlayState = PlaybackStateCompat.STATE_NONE; + + private TomahawkMediaPlayerCallback mMediaPlayerCallback; + + private class CompletionListener implements MediaPlayer.OnCompletionListener { + + public void onCompletion(MediaPlayer mp) { + Runnable r = new Runnable() { + @Override + public void run() { + Log.d(TAG, "onCompletion()"); + if (mMediaPlayerCallback != null) { + mMediaPlayerCallback.onCompletion(AndroidMediaPlayer.this, mPreparedQuery); + } else { + Log.e(TAG, + "Wasn't able to call onCompletion because callback object is null"); + } + } + }; + ThreadManager.get().executePlayback(AndroidMediaPlayer.this, r); + } + } + + @Override + public void play() { + mPlayState = PlaybackStateCompat.STATE_PLAYING; + handlePlayState(); + } + + @Override + public void pause() { + mPlayState = PlaybackStateCompat.STATE_PAUSED; + handlePlayState(); + } + + @Override + public void seekTo(long msec) { + if (sMediaPlayer != null) { + try { + sMediaPlayer.seekTo((int) msec); + } catch (IllegalStateException e) { + //ignored + } + } + } + + @Override + public void prepare(final Query query, final TomahawkMediaPlayerCallback callback) { + Log.d(TAG, "prepare() query: " + query); + mMediaPlayerCallback = callback; + mPreparedQuery = null; + mPreparingQuery = query; + if (sMediaPlayer != null) { + try { + sMediaPlayer.stop(); + } catch (IllegalStateException e) { + //ignored + } + } + getStreamUrl(query.getPreferredTrackResult()).done(new DoneCallback() { + @Override + public void onDone(String url) { + Log.d(TAG, "Received stream url: " + url + " for query: " + query); + if (mPreparingQuery != null && mPreparingQuery == query) { + Log.d(TAG, "Starting to prepare stream url: " + url + " for query: " + query); + if (sMediaPlayer != null) { + try { + sMediaPlayer.stop(); + } catch (IllegalStateException e) { + //ignored + } + sMediaPlayer.release(); + } + sMediaPlayer = new MediaPlayer(); + + sMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + + try { + sMediaPlayer.setDataSource(url); + sMediaPlayer.prepare(); + } catch (IOException | IllegalStateException e) { + Log.e(TAG, "prepare - ", e); + callback.onError(AndroidMediaPlayer.this, "MediaPlayerEncounteredError"); + } + + sMediaPlayer.setOnCompletionListener(new CompletionListener()); + + mPreparedQuery = mPreparingQuery; + mPreparingQuery = null; + handlePlayState(); + callback.onPrepared(AndroidMediaPlayer.this, mPreparedQuery); + Log.d(TAG, "onPrepared() url: " + url + " for query: " + query); + } else { + Log.d(TAG, "Ignoring stream url: " + url + " for query: " + query + + ", because preparing query is: " + mPreparingQuery); + } + } + }); + } + + @Override + public void release() { + Log.d(TAG, "release"); + mPreparedQuery = null; + mPreparingQuery = null; + if (sMediaPlayer != null) { + try { + sMediaPlayer.stop(); + } catch (IllegalStateException e) { + //ignored + } + sMediaPlayer.release(); + } + mMediaPlayerCallback = null; + } + + @Override + public long getPosition() { + if (sMediaPlayer != null) { + try { + return sMediaPlayer.getCurrentPosition(); + } catch (IllegalStateException e) { + //ignored + } + } + return 0; + } + + @Override + public void setBitrate(int mode) { + } + + @Override + public boolean isPlaying(Query query) { + try { + return sMediaPlayer != null && sMediaPlayer.isPlaying(); + } catch (IllegalStateException e) { + //ignored + } + return false; + } + + @Override + public boolean isPreparing(Query query) { + return mPreparingQuery != null && mPreparingQuery == query; + } + + @Override + public boolean isPrepared(Query query) { + return mPreparedQuery != null && mPreparedQuery == query; + } + + private void handlePlayState() { + if (sMediaPlayer != null && mPreparedQuery != null) { + try { + if (mPlayState == PlaybackStateCompat.STATE_PAUSED + && sMediaPlayer.isPlaying()) { + sMediaPlayer.pause(); + } else if (mPlayState == PlaybackStateCompat.STATE_PLAYING + && !sMediaPlayer.isPlaying()) { + sMediaPlayer.start(); + } + } catch (IllegalStateException e) { + //ignored + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/DeezerMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/DeezerMediaPlayer.java new file mode 100644 index 000000000..489332f1e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/DeezerMediaPlayer.java @@ -0,0 +1,79 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.ScriptJob; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverAccessTokenResult; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.os.Build; +import android.os.Bundle; + +public class DeezerMediaPlayer extends PluginMediaPlayer { + + private static final String TAG = DeezerMediaPlayer.class.getSimpleName(); + + public static final String PACKAGE_NAME = "org.tomahawk.deezerplugin"; + + public static final int MIN_VERSION = 20; + + public DeezerMediaPlayer() { + super(TomahawkApp.PLUGINNAME_DEEZER, PACKAGE_NAME); + } + + public static String getPluginDownloadLink() { + if (Build.CPU_ABI.equals("x86")) { + return "http://download.tomahawk-player.org/android-plugins/" + + "tomahawk-android-deezer-x86-release-" + MIN_VERSION + ".apk"; + } else { + return "http://download.tomahawk-player.org/android-plugins/" + + "tomahawk-android-deezer-armv7a-release-" + MIN_VERSION + ".apk"; + } + } + + @Override + public String getUri(Query query) { + String strippedPath = query.getPreferredTrackResult().getPath() + .replace("deezer://track/", ""); + String[] parts = strippedPath.split("/"); + return parts[0]; + } + + @Override + public void prepare(final String uri) { + getScriptResolver().getAccessToken( + new ScriptJob.ResultsCallback( + ScriptResolverAccessTokenResult.class) { + @Override + public void onReportResults(ScriptResolverAccessTokenResult results) { + Bundle args = new Bundle(); + args.putString(MSG_PREPARE_ARG_URI, uri); + args.putString(MSG_PREPARE_ARG_ACCESSTOKEN, results.accessToken); + args.putLong(MSG_PREPARE_ARG_ACCESSTOKENEXPIRES, + results.accessTokenExpires); + callService(MSG_PREPARE, args); + } + }); + } + + @Override + public void setBitrate(int bitrateMode) { + // The Deezer Android SDK doesn't support setting a bitrate + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/PluginMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/PluginMediaPlayer.java new file mode 100644 index 000000000..29913d728 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/PluginMediaPlayer.java @@ -0,0 +1,487 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.WeakReferenceHandler; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.greenrobot.event.EventBus; + +public abstract class PluginMediaPlayer extends TomahawkMediaPlayer { + + private static final String TAG = PluginMediaPlayer.class.getSimpleName(); + + /** + * Command to the service to register a client, receiving callbacks from the service. The + * Message's replyTo field must be a Messenger of the client where callbacks should be sent. + */ + public static final int MSG_REGISTER_CLIENT = 1; + + /** + * Command to the service to unregister a client, ot stop receiving callbacks from the service. + * The Message's replyTo field must be a Messenger of the client as previously given with + * MSG_REGISTER_CLIENT. + */ + public static final int MSG_UNREGISTER_CLIENT = 2; + + /** + * Commands to the service + */ + protected static final int MSG_PREPARE = 100; + + protected static final String MSG_PREPARE_ARG_URI = "uri"; + + protected static final String MSG_PREPARE_ARG_ACCESSTOKEN = "accessToken"; + + protected static final String MSG_PREPARE_ARG_ACCESSTOKENEXPIRES = "accessTokenExpires"; + + protected static final int MSG_PLAY = 101; + + protected static final int MSG_PAUSE = 102; + + protected static final int MSG_SEEK = 103; + + protected static final String MSG_SEEK_ARG_MS = "ms"; + + protected static final int MSG_SETBITRATE = 104; + + protected static final String MSG_SETBITRATE_ARG_MODE = "mode"; + + /** + * Commands to the client + */ + protected static final int MSG_ONPAUSE = 200; + + protected static final int MSG_ONPLAY = 201; + + protected static final int MSG_ONPREPARED = 202; + + protected static final String MSG_ONPREPARED_ARG_URI = "uri"; + + protected static final int MSG_ONPLAYERENDOFTRACK = 203; + + protected static final int MSG_ONPLAYERPOSITIONCHANGED = 204; + + protected static final String MSG_ONPLAYERPOSITIONCHANGED_ARG_POSITION = "position"; + + protected static final String MSG_ONPLAYERPOSITIONCHANGED_ARG_TIMESTAMP = "timestamp"; + + protected static final int MSG_ONERROR = 205; + + protected static final String MSG_ONERROR_ARG_MESSAGE = "message"; + + private String mPluginName; + + private String mPackageName; + + private boolean mIsRequestingService = false; + + /** + * Messenger for communicating with service. + */ + private Messenger mService = null; + + private List mWaitingMessages = new ArrayList<>(); + + private TomahawkMediaPlayerCallback mMediaPlayerCallback; + + private boolean mIsPlaying; + + private int mPlayState = PlaybackStateCompat.STATE_NONE; + + private Query mPreparedQuery; + + private Query mPreparingQuery; + + private Query mActuallyPreparingQuery; + + private boolean mShowFakePosition = false; + + private final DisableFakePositionHandler mDisableFakePositionHandler + = new DisableFakePositionHandler(this); + + private static class DisableFakePositionHandler + extends WeakReferenceHandler { + + public DisableFakePositionHandler(PluginMediaPlayer referencedObject) { + super(referencedObject); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + getReferencedObject().mShowFakePosition = false; + } + } + } + + private ServiceConnection mConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, + IBinder service) { + // We want to monitor the service for as long as we are + // connected to it. + try { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. We are communicating with our + // service through an IDL interface, so get a client-side + // representation of that from the raw service object. + Messenger messenger = new Messenger(service); + Message msg = Message.obtain(null, PluginMediaPlayer.MSG_REGISTER_CLIENT); + msg.replyTo = getReceivingMessenger(); + messenger.send(msg); + setService(messenger); + Log.d(TAG, "Successfully attached to service! :)"); + } catch (RemoteException e) { + // In this case the service has crashed before we could even + // do anything with it; we can count on soon being + // disconnected (and then reconnected if it can be restarted) + // so there is no need to do anything here. + Log.e(TAG, "Service crashed before we could do anything." + + " Waiting for it to restart and report for duty..."); + } + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + setService(null); + Log.e(TAG, "Service crashed :("); + } + }; + + private boolean mRestorePosition = false; + + private String mPreparedUri; + + private long mPositionTimeStamp; + + private int mPositionOffset; + + private long mFakePositionTimeStamp; + + private long mFakePositionOffset; + + private Map mUriToQueryMap = new HashMap<>(); + + public PluginMediaPlayer(String pluginName, String packageName) { + mPluginName = pluginName; + mPackageName = packageName; + } + + public ScriptResolver getScriptResolver() { + ScriptResolver scriptResolver = PipeLine.get().getResolver(mPluginName); + if (scriptResolver == null) { + Log.e(TAG, "getScriptResolver - Couldn't find associated ScriptResolver!"); + } + return scriptResolver; + } + + /** + * Handler of incoming messages from service. + */ + private static class IncomingHandler extends WeakReferenceHandler { + + public IncomingHandler(PluginMediaPlayer referencedObject) { + super(referencedObject); + } + + @Override + public void handleMessage(Message msg) { + PluginMediaPlayer mp = getReferencedObject(); + switch (msg.what) { + case MSG_ONPREPARED: + String uri = msg.getData().getString(MSG_ONPREPARED_ARG_URI); + Log.d(TAG, "onPrepared() - uri: " + uri); + if (mp.mPreparingQuery != null + && mp.mActuallyPreparingQuery == mp.mPreparingQuery) { + mp.mActuallyPreparingQuery = null; + mp.mPreparedQuery = mp.mUriToQueryMap.get(uri); + mp.mPreparingQuery = null; + if (mp.mMediaPlayerCallback != null) { + mp.mMediaPlayerCallback.onPrepared(mp, mp.mPreparedQuery); + } else { + Log.e(TAG, + "Wasn't able to call onPrepared because callback object is null"); + } + mp.handlePlayState(); + if (mp.mRestorePosition && mp.mPreparedUri != null + && mp.mPreparedUri.equals(uri)) { + mp.mRestorePosition = false; + mp.seekTo(mp.mPositionOffset); + } else { + mp.mPositionOffset = 0; + mp.mPositionTimeStamp = System.currentTimeMillis(); + } + mp.mPreparedUri = uri; + } + break; + case MSG_ONPLAY: + mp.mIsPlaying = true; + mp.mPositionTimeStamp = System.currentTimeMillis(); + break; + case MSG_ONPAUSE: + mp.mIsPlaying = false; + mp.mPositionOffset = + (int) (System.currentTimeMillis() - mp.mPositionTimeStamp) + + mp.mPositionOffset; + mp.mPositionTimeStamp = System.currentTimeMillis(); + break; + case MSG_ONPLAYERPOSITIONCHANGED: + long timeStamp = + msg.getData().getLong(MSG_ONPLAYERPOSITIONCHANGED_ARG_TIMESTAMP); + int position = + msg.getData().getInt(MSG_ONPLAYERPOSITIONCHANGED_ARG_POSITION); + + mp.mPositionTimeStamp = timeStamp; + mp.mPositionOffset = position; + break; + case MSG_ONPLAYERENDOFTRACK: + Log.d(TAG, "onCompletion()"); + if (mp.mMediaPlayerCallback != null) { + mp.mMediaPlayerCallback.onCompletion(mp, mp.mPreparedQuery); + } else { + Log.e(TAG, + "Wasn't able to call onCompletion because callback object is null"); + } + break; + case MSG_ONERROR: + String message = msg.getData().getString(MSG_ONERROR_ARG_MESSAGE); + + if (mp.mMediaPlayerCallback != null) { + mp.mMediaPlayerCallback.onError(mp, message); + } else { + Log.e(TAG, "Wasn't able to call onError because callback object is null"); + } + default: + super.handleMessage(msg); + } + } + } + + /** + * Target we publish for clients to send messages to IncomingHandler. + */ + final Messenger mReceivingMessenger = new Messenger(new IncomingHandler(this)); + + public Messenger getReceivingMessenger() { + return mReceivingMessenger; + } + + protected synchronized void callService(int what) { + callService(what, null); + } + + protected synchronized void callService(int what, Bundle bundle) { + Message message = Message.obtain(null, what); + message.setData(bundle); + callService(message); + } + + private synchronized void callService(Message message) { + if (mService != null) { + try { + mService.send(message); + } catch (RemoteException e) { + Log.e(TAG, "Service crashed: ", e); + mWaitingMessages.add(message); + } + } else { + // cache the message, will be send in setService + mWaitingMessages.add(message); + if (!mIsRequestingService) { + mIsRequestingService = true; + requestService(); + } + } + } + + /** + * Construct and send off a {@link PlaybackService.RequestServiceBindingEvent} to the {@link + * PlaybackService}. As soon as the {@link PlaybackService} has been successfully bound to the + * PluginService {@link #setService} will be called. + */ + private void requestService() { + PlaybackService.RequestServiceBindingEvent event = + new PlaybackService.RequestServiceBindingEvent(mConnection, mPackageName); + EventBus.getDefault().post(event); + } + + public synchronized void setService(Messenger service) { + mIsRequestingService = false; + mService = service; + if (mService != null) { + // send all cached messages + while (!mWaitingMessages.isEmpty()) { + callService(mWaitingMessages.remove(0)); + } + } else { + mRestorePosition = true; + mPreparedQuery = null; + mPreparingQuery = null; + mIsPlaying = false; + } + } + + public synchronized boolean isBound() { + return mService != null; + } + + public ServiceConnection getServiceConnection() { + return mConnection; + } + + public abstract String getUri(Query query); + + public abstract void prepare(String uri); + + /** + * Prepare the given {@link Query} for playback + * + * @param query the {@link Query} that should be prepared for playback + * @param callback a {@link TomahawkMediaPlayerCallback} that should be stored and be used to + * report certain callbacks back to the {@link PlaybackService} + */ + @Override + public void prepare(Query query, TomahawkMediaPlayerCallback callback) { + Log.d(TAG, "prepare()"); + mMediaPlayerCallback = callback; + mPreparedQuery = null; + mPreparingQuery = query; + mActuallyPreparingQuery = query; + callService(MSG_PAUSE); + + String uri = getUri(query); + mUriToQueryMap.put(uri, query); + prepare(uri); + } + + /** + * Start playing the previously prepared {@link Query} + */ + @Override + public void play() { + Log.d(TAG, "play()"); + mPlayState = PlaybackStateCompat.STATE_PLAYING; + handlePlayState(); + } + + /** + * Pause playing the current {@link Query} + */ + @Override + public void pause() { + Log.d(TAG, "pause()"); + mPlayState = PlaybackStateCompat.STATE_PAUSED; + handlePlayState(); + } + + /** + * Seek to the given playback position (in ms) + */ + @Override + public void seekTo(final long msec) { + Log.d(TAG, "seekTo()"); + Bundle args = new Bundle(); + args.putInt(MSG_SEEK_ARG_MS, (int) msec); + callService(MSG_SEEK, args); + + mFakePositionOffset = msec; + mFakePositionTimeStamp = System.currentTimeMillis(); + mShowFakePosition = true; + // After 1 second, we set mShowFakePosition to false again + mDisableFakePositionHandler.sendEmptyMessageDelayed(1337, 1000); + } + + /** + * Release any relevant resources that this {@link PluginMediaPlayer} might hold onto + */ + @Override + public void release() { + Log.d(TAG, "release()"); + mPreparedQuery = null; + mPreparingQuery = null; + callService(MSG_PAUSE); + mMediaPlayerCallback = null; + } + + /** + * @return the current track position + */ + @Override + public long getPosition() { + if (mShowFakePosition) { + if (mIsPlaying) { + return System.currentTimeMillis() - mFakePositionTimeStamp + mFakePositionOffset; + } else { + return mFakePositionOffset; + } + } else { + if (mIsPlaying) { + return System.currentTimeMillis() - mPositionTimeStamp + mPositionOffset; + } else { + return mPositionOffset; + } + } + } + + @Override + public boolean isPlaying(Query query) { + return mPreparedQuery == query && mIsPlaying; + } + + @Override + public boolean isPreparing(Query query) { + return mPreparingQuery == query; + } + + @Override + public boolean isPrepared(Query query) { + return mPreparedQuery == query; + } + + private void handlePlayState() { + if (mPreparedQuery != null) { + if (mPlayState == PlaybackStateCompat.STATE_PAUSED && mIsPlaying) { + callService(MSG_PAUSE); + } else if (mPlayState == PlaybackStateCompat.STATE_PLAYING && !mIsPlaying) { + callService(MSG_PLAY); + } + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/SpotifyMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/SpotifyMediaPlayer.java new file mode 100644 index 000000000..5a7c1ede6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/SpotifyMediaPlayer.java @@ -0,0 +1,103 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.ScriptJob; +import org.tomahawk.libtomahawk.resolver.models.ScriptResolverAccessTokenResult; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +/** + * This class wraps all functionality to be able to directly playback spotify-resolved tracks with + * OpenSLES . + */ +public class SpotifyMediaPlayer extends PluginMediaPlayer { + + private static final String TAG = SpotifyMediaPlayer.class.getSimpleName(); + + public static final String PACKAGE_NAME = "org.tomahawk.spotifyplugin"; + + public static final int MIN_VERSION = 42; + + public static final String CURRENT_VERSION_NAME = "0.53"; + + public SpotifyMediaPlayer() { + super(TomahawkApp.PLUGINNAME_SPOTIFY, PACKAGE_NAME); + } + + public static String getPluginDownloadLink() { + if (Build.CPU_ABI.equals("x86")) { + return "http://download.tomahawk-player.org/android-plugins/" + + "tomahawk-android-spotify-x86-release-" + CURRENT_VERSION_NAME + ".apk"; + } else { + return "http://download.tomahawk-player.org/android-plugins/" + + "tomahawk-android-spotify-armv7a-release-" + CURRENT_VERSION_NAME + ".apk"; + } + } + + @Override + public String getUri(Query query) { + String[] pathParts = + query.getPreferredTrackResult().getPath().split("/"); + return "spotify:track:" + pathParts[pathParts.length - 1]; + } + + @Override + public void prepare(final String uri) { + getScriptResolver().getAccessToken( + new ScriptJob.ResultsCallback( + ScriptResolverAccessTokenResult.class) { + @Override + public void onReportResults(ScriptResolverAccessTokenResult results) { + Bundle args = new Bundle(); + args.putString(MSG_PREPARE_ARG_URI, uri); + args.putString(MSG_PREPARE_ARG_ACCESSTOKEN, results.accessToken); + callService(MSG_PREPARE, args); + } + }); + } + + @Override + public void setBitrate(int bitrateMode) { + Bundle args = new Bundle(); + args.putInt(MSG_SETBITRATE_ARG_MODE, bitrateMode); + callService(MSG_SETBITRATE, args); + } + + public void updateBitrate() { + ConnectivityManager conMan = (ConnectivityManager) TomahawkApp.getContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = conMan.getActiveNetworkInfo(); + if (netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI) { + Log.d(TAG, "Updating bitrate to HIGH, because we have a Wifi connection"); + setBitrate(PreferenceUtils.PREF_BITRATE_HIGH); + } else { + Log.d(TAG, "Updating bitrate to user setting, because we don't have a Wifi connection"); + int prefbitrate = PreferenceUtils.getInt(PreferenceUtils.PREF_BITRATE); + setBitrate(prefbitrate); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayer.java new file mode 100644 index 000000000..550d6a56d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayer.java @@ -0,0 +1,86 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.jdeferred.DoneCallback; +import org.jdeferred.FailCallback; +import org.jdeferred.Promise; +import org.jdeferred.impl.DeferredObject; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.Result; +import org.tomahawk.libtomahawk.resolver.ScriptResolver; +import org.tomahawk.tomahawk_android.utils.ThreadManager; + +public abstract class TomahawkMediaPlayer { + + public abstract void play(); + + public abstract void pause(); + + public abstract void seekTo(long msec); + + public abstract void prepare(Query query, TomahawkMediaPlayerCallback callback); + + public abstract void release(); + + public abstract long getPosition(); + + public abstract void setBitrate(int mode); + + public abstract boolean isPlaying(Query query); + + public abstract boolean isPreparing(Query query); + + public abstract boolean isPrepared(Query query); + + public Promise getStreamUrl(Result result) { + final DeferredObject deferred = new DeferredObject<>(); + if (result.getResolvedBy() instanceof ScriptResolver) { + ScriptResolver resolver = (ScriptResolver) result.getResolvedBy(); + resolver.getStreamUrl(result) + .done(new DoneCallback() { + @Override + public void onDone(final String url) { + Runnable r = new Runnable() { + @Override + public void run() { + deferred.resolve(url); + } + }; + ThreadManager.get().executePlayback(TomahawkMediaPlayer.this, r); + } + }) + .fail(new FailCallback() { + @Override + public void onFail(final Throwable result) { + Runnable r = new Runnable() { + @Override + public void run() { + deferred.reject(result); + } + }; + ThreadManager.get().executePlayback(TomahawkMediaPlayer.this, r); + } + }); + } else { + deferred.resolve(result.getPath()); + } + return deferred; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayerCallback.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayerCallback.java new file mode 100644 index 000000000..149cd07d6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/TomahawkMediaPlayerCallback.java @@ -0,0 +1,41 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.tomahawk.libtomahawk.resolver.Query; + +public interface TomahawkMediaPlayerCallback { + + /** + * Called as soon as a {@link Query} has been prepared + */ + void onPrepared(TomahawkMediaPlayer mediaPlayer, Query query); + + /** + * Called if playback of the currently prepared {@link Query} has finished. + */ + void onCompletion(TomahawkMediaPlayer mediaPlayer, Query query); + + /** + * Called whenever an error occurred. + * + * @param message String containing error details + */ + void onError(TomahawkMediaPlayer mediaPlayer, String message); + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/VLCMediaPlayer.java b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/VLCMediaPlayer.java new file mode 100644 index 000000000..4d40d9a37 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/mediaplayers/VLCMediaPlayer.java @@ -0,0 +1,235 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.mediaplayers; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.utils.ThreadManager; +import org.videolan.libvlc.LibVLC; +import org.videolan.libvlc.Media; +import org.videolan.libvlc.MediaPlayer; +import org.videolan.libvlc.util.AndroidUtil; + +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import java.util.ArrayList; + +/** + * This class wraps a libvlc mediaplayer instance. + */ +public class VLCMediaPlayer extends TomahawkMediaPlayer { + + private static final String TAG = VLCMediaPlayer.class.getSimpleName(); + + private static MediaPlayer sMediaPlayer; + + private static LibVLC sLibVLC; + + static { + ArrayList options = new ArrayList<>(); + options.add("--http-reconnect"); + options.add("--network-caching=2000"); + sLibVLC = new LibVLC(options); + sMediaPlayer = new MediaPlayer(sLibVLC); + } + + private TomahawkMediaPlayerCallback mMediaPlayerCallback; + + private Query mPreparedQuery; + + private Query mPreparingQuery; + + private int mPlayState = PlaybackStateCompat.STATE_NONE; + + private class MediaPlayerListener implements MediaPlayer.EventListener { + + @Override + public void onEvent(final MediaPlayer.Event event) { + Runnable r = new Runnable() { + @Override + public void run() { + switch (event.type) { + case MediaPlayer.Event.EncounteredError: + Log.d(TAG, "MediaPlayer.Event.EncounteredError"); + mPreparedQuery = null; + mPreparingQuery = null; + if (mMediaPlayerCallback != null) { + mMediaPlayerCallback.onError( + VLCMediaPlayer.this, "LibVLC encountered an error"); + } else { + Log.e(TAG, "Wasn't able to call onError because callback" + + " object is null"); + } + break; + case MediaPlayer.Event.EndReached: + Log.d(TAG, "MediaPlayer.Event.EndReached"); + if (mMediaPlayerCallback != null) { + mMediaPlayerCallback.onCompletion( + VLCMediaPlayer.this, mPreparedQuery); + } else { + Log.e(TAG, "Wasn't able to call onCompletion because callback" + + " object is null"); + } + break; + } + } + }; + ThreadManager.get().executePlayback(VLCMediaPlayer.this, r); + } + } + + public VLCMediaPlayer() { + sMediaPlayer = new MediaPlayer(sLibVLC); + if (PreferenceUtils.getBoolean(PreferenceUtils.EQUALIZER_ENABLED)) { + MediaPlayer.Equalizer equalizer = MediaPlayer.Equalizer.create(); + float[] bands = PreferenceUtils.getFloatArray(PreferenceUtils.EQUALIZER_VALUES); + equalizer.setPreAmp(bands[0]); + for (int i = 0; i < MediaPlayer.Equalizer.getBandCount(); i++) { + equalizer.setAmp(i, bands[i + 1]); + } + sMediaPlayer.setEqualizer(equalizer); + } + sMediaPlayer.setEventListener(new MediaPlayerListener()); + } + + public static LibVLC getLibVlcInstance() { + return sLibVLC; + } + + public static MediaPlayer getMediaPlayerInstance() { + return sMediaPlayer; + } + + /** + * Start playing the previously prepared {@link org.tomahawk.libtomahawk.collection.Track} + */ + @Override + public void play() throws IllegalStateException { + Log.d(TAG, "play()"); + mPlayState = PlaybackStateCompat.STATE_PLAYING; + handlePlayState(); + } + + /** + * Pause playing the current {@link org.tomahawk.libtomahawk.collection.Track} + */ + @Override + public void pause() throws IllegalStateException { + Log.d(TAG, "pause()"); + mPlayState = PlaybackStateCompat.STATE_PAUSED; + handlePlayState(); + } + + /** + * Seek to the given playback position (in ms) + */ + @Override + public void seekTo(long msec) throws IllegalStateException { + Log.d(TAG, "seekTo()"); + if (mPreparedQuery != null && !TomahawkApp.PLUGINNAME_BEATSMUSIC.equals( + mPreparedQuery.getPreferredTrackResult().getResolvedBy().getId())) { + getMediaPlayerInstance().setTime(msec); + } + } + + /** + * Prepare the given url + */ + @Override + public void prepare(final Query query, TomahawkMediaPlayerCallback callback) { + Log.d(TAG, "prepare() query: " + query); + mMediaPlayerCallback = callback; + getMediaPlayerInstance().stop(); + mPreparedQuery = null; + mPreparingQuery = query; + getStreamUrl(query.getPreferredTrackResult()).done(new DoneCallback() { + @Override + public void onDone(String url) { + Log.d(TAG, "Received stream url: " + url + " for query: " + query); + if (mPreparingQuery != null && mPreparingQuery == query) { + Log.d(TAG, "Starting to prepare stream url: " + url + " for query: " + query); + Media media = new Media(sLibVLC, AndroidUtil.LocationToUri(url)); + getMediaPlayerInstance().setMedia(media); + mPreparedQuery = mPreparingQuery; + mPreparingQuery = null; + mMediaPlayerCallback.onPrepared(VLCMediaPlayer.this, mPreparedQuery); + handlePlayState(); + Log.d(TAG, "onPrepared() url: " + url + " for query: " + query); + } else { + Log.d(TAG, "Ignoring stream url: " + url + " for query: " + query + + ", because preparing query is: " + mPreparingQuery); + } + } + }); + } + + @Override + public void release() { + Log.d(TAG, "release()"); + mPreparedQuery = null; + mPreparingQuery = null; + getMediaPlayerInstance().stop(); + mMediaPlayerCallback = null; + } + + /** + * @return the current track position + */ + @Override + public long getPosition() { + if (mPreparedQuery != null) { + return getMediaPlayerInstance().getTime(); + } else { + return 0L; + } + } + + @Override + public void setBitrate(int bitrateMode) { + } + + @Override + public boolean isPlaying(Query query) { + return isPrepared(query) && getMediaPlayerInstance().isPlaying(); + } + + @Override + public boolean isPreparing(Query query) { + return mPreparingQuery != null && mPreparingQuery == query; + } + + @Override + public boolean isPrepared(Query query) { + return mPreparedQuery != null && mPreparedQuery == query; + } + + private void handlePlayState() { + if (mPreparedQuery != null) { + if (mPlayState == PlaybackStateCompat.STATE_PAUSED + && getMediaPlayerInstance().isPlaying()) { + getMediaPlayerInstance().pause(); + } else if (mPlayState == PlaybackStateCompat.STATE_PLAYING + && !getMediaPlayerInstance().isPlaying()) { + getMediaPlayerInstance().play(); + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AbstractPlayStatusReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AbstractPlayStatusReceiver.java new file mode 100644 index 000000000..97771fb0c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AbstractPlayStatusReceiver.java @@ -0,0 +1,142 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +/** + * Base class for play status receivers. + * + * @author tgwizard + * @author mrmaffen + */ +public abstract class AbstractPlayStatusReceiver extends BroadcastReceiver { + + private static final String TAG = AbstractPlayStatusReceiver.class.getSimpleName(); + + private Intent mServiceIntent = null; + + private Track mTrack; + + @Override + public final void onReceive(Context context, Intent intent) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + String action = intent.getAction(); + Bundle bundle = intent.getExtras(); + + Log.v(TAG, "Action received was: " + action); + + // check to make sure we actually got something + if (action == null) { + Log.w(TAG, "Got null action"); + return; + } + + if (bundle == null) { + bundle = Bundle.EMPTY; + } + + mServiceIntent = new Intent(MicroService.ACTION_PLAYSTATECHANGED); + + try { + parseIntent(context, action, bundle); // might throw + + // parseIntent must have called setTrack with non-null values + if (mTrack == null) { + throw new IllegalArgumentException( + "Track was null, not starting/calling MicroService"); + } else { + mServiceIntent.putExtra(MicroService.EXTRA_TRACKKEY, mTrack.getCacheKey()); + // start/call the Scrobbling Service + context.startService(mServiceIntent); + } + } catch (IllegalArgumentException e) { + Log.i(TAG, "onReceive: Got a bad track, ignoring it (" + e.getMessage() + ")"); + } + } + } + + /** + * Sets a boolean indicating, that the received track is the same as the current one. + */ + protected final void setIsSameAsCurrentTrack() { + mServiceIntent.putExtra(MicroService.EXTRA_IS_SAME_AS_CURRENT_TRACK, true); + } + + /** + * Sets the music brains id of the track in the received broadcast. + */ + protected final void setMbid(String mbid) { + mServiceIntent.putExtra(MicroService.EXTRA_MBID, mbid); + } + + /** + * Sets the source that the broadcast has been received from. + */ + protected final void setSource(String source) { + mServiceIntent.putExtra(MicroService.EXTRA_SOURCE, source); + } + + /** + * Sets the timestamp that the broadcast has been received at. + */ + protected final void setTimestamp(long timestamp) { + mServiceIntent.putExtra(MicroService.EXTRA_TIMESTAMP, timestamp); + } + + /** + * Sets the {@link org.tomahawk.tomahawk_android.services.MicroService.State} that the received + * broadcast represents. + */ + protected final void setState(MicroService.State state) { + mServiceIntent.putExtra(MicroService.EXTRA_STATE, state.name()); + } + + /** + * Sets the {@link Track} for this scrobble request + * + * @param track the Track for this scrobble request + */ + protected final void setTrack(Track track) { + mTrack = track; + } + + /** + * Parses the API / music app specific parts of the received broadcast. + * + * @param ctx to be able to create {@code MusicAPIs} + * @param action the action/intent used for this scrobble request + * @param bundle the data sent with this request + * @throws IllegalArgumentException when the data received is invalid + * @see #setTrack(Track) + */ + protected abstract void parseIntent(Context ctx, String action, + Bundle bundle) throws IllegalArgumentException; + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicJRTStudioBuildReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicJRTStudioBuildReceiver.java new file mode 100644 index 000000000..fe3974590 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicJRTStudioBuildReceiver.java @@ -0,0 +1,48 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the Android Music Player. + * + * @author grodin + * @see AbstractPlayStatusReceiver + * @since 1.4.3 + */ +public class AndroidMusicJRTStudioBuildReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_ANDROID_PLAYSTATECHANGED + = "com.jrtstudio.music.playstatechanged"; + + public static final String ACTION_ANDROID_STOP = "com.jrtstudio.music.playbackcomplete"; + + public static final String ACTION_ANDROID_METACHANGED = "com.jrtstudio.music.metachanged"; + + public static final String PACKAGE_NAME = "com.jrtstudio.music"; + + public static final String NAME = "Android Music Player (JRT Studio Build)"; + + static final String GOOGLE_MUSIC_PACKAGE = "com.jrtstudio.music"; + + public AndroidMusicJRTStudioBuildReceiver() { + super(ACTION_ANDROID_STOP, PACKAGE_NAME, NAME); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicReceiver.java new file mode 100644 index 000000000..8afc7dc4b --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/AndroidMusicReceiver.java @@ -0,0 +1,48 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the Android Music Player. + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.0.1 + */ +public class AndroidMusicReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_ANDROID_PLAYSTATECHANGED + = "com.android.music.playstatechanged"; + + public static final String ACTION_ANDROID_STOP = "com.android.music.playbackcomplete"; + + public static final String ACTION_ANDROID_METACHANGED = "com.android.music.metachanged"; + + public static final String PACKAGE_NAME = "com.android.music"; + + public static final String NAME = "Android Music Player"; + + static final String GOOGLE_MUSIC_PACKAGE = "com.google.android.music"; + + public AndroidMusicReceiver() { + super(ACTION_ANDROID_STOP, PACKAGE_NAME, NAME); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BootCompletedReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BootCompletedReceiver.java new file mode 100644 index 000000000..2953f422a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BootCompletedReceiver.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class BootCompletedReceiver extends BroadcastReceiver { + + private static final String TAG = BootCompletedReceiver.class.getSimpleName(); + + @Override + public final void onReceive(Context context, Intent intent) { + Log.d(TAG, "Received intent: " + intent.getAction()); + + context.startService(new Intent(context, MicroService.class)); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BuiltInMusicAppReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BuiltInMusicAppReceiver.java new file mode 100644 index 000000000..9a698589a --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/BuiltInMusicAppReceiver.java @@ -0,0 +1,103 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +/** + * A BroadcastReceiver for intents sent by music apps such as Android Music and Hero Music. + * Specialized classes inherit from this class to deal with the small differences. + * + * @author tgwizard + * @see AndroidMusicReceiver + * @see HeroMusicReceiver + * @since 1.2.7 + */ +public abstract class BuiltInMusicAppReceiver extends + AbstractPlayStatusReceiver { + + private static final String TAG = BuiltInMusicAppReceiver.class.getSimpleName(); + + final String stop_action; + + final String app_package; + + final String app_name; + + public BuiltInMusicAppReceiver(String stopAction, String appPackage, + String appName) { + super(); + stop_action = stopAction; + app_package = appPackage; + app_name = appName; + } + + /** + * Depending on the action received decide whether it should signal a stop or not. By default, + * it compares it to the unique `this.stop_action`, but there might be multiple actions that + * cause a stop signal. + * + * @param action the received action + * @return true when the received action is a stop action, false otherwise + */ + protected boolean isStopAction(String action) { + return action.equals(stop_action); + } + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + setTimestamp(System.currentTimeMillis()); + + if (isStopAction(action)) { + setState(MicroService.State.PLAYLIST_FINISHED); + } else { + setState(MicroService.State.RESUME); + } + + setTrack(parseTrack(bundle)); + } + + Track parseTrack(Bundle bundle) { + Log.d(TAG, "Will read data from intent"); + + CharSequence artistName = bundle.getCharSequence("artist"); + CharSequence albumName = bundle.getCharSequence("album"); + CharSequence trackname = bundle.getCharSequence("track"); + if (artistName == null || trackname == null) { + throw new IllegalArgumentException("null track values"); + } + + Artist artist = Artist.get(artistName.toString()); + Album album = null; + if (albumName != null) { + album = Album.get(albumName.toString(), artist); + } + return Track.get(trackname.toString(), album, artist); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/DoubleTwistReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/DoubleTwistReceiver.java new file mode 100644 index 000000000..134c633af --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/DoubleTwistReceiver.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +public class DoubleTwistReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_ANDROID_PLAYSTATECHANGED + = "com.doubleTwist.androidPlayer.playstatechanged"; + + public static final String ACTION_ANDROID_STOP + = "com.doubleTwist.androidPlayer.playbackcomplete"; + + public static final String ACTION_ANDROID_METACHANGED + = "com.doubleTwist.androidPlayer.metachanged"; + + public static final String PACKAGE_NAME = "com.doubleTwist.androidPlayer"; + + public static final String NAME = "DoubleTwist"; + + public DoubleTwistReceiver() { + super(ACTION_ANDROID_STOP, PACKAGE_NAME, NAME); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/HeroMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/HeroMusicReceiver.java new file mode 100644 index 000000000..95e75ecce --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/HeroMusicReceiver.java @@ -0,0 +1,41 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the HTC Hero Music Player. + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.0.1 + */ +public class HeroMusicReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_HTC_PLAYSTATECHANGED = "com.htc.music.playstatechanged"; + + public static final String ACTION_HTC_STOP = "com.htc.music.playbackcomplete"; + + public static final String ACTION_HTC_METACHANGED = "com.htc.music.metachanged"; + + public HeroMusicReceiver() { + super(ACTION_HTC_STOP, "com.htc.music", "Hero Music Player"); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LastFmAPIReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LastFmAPIReceiver.java new file mode 100644 index 000000000..550374f8f --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LastFmAPIReceiver.java @@ -0,0 +1,81 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +/** + * A BroadcastReceiver for the Last.fm Android API. More info at their dev page + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.3.2 + */ +public class LastFmAPIReceiver extends AbstractPlayStatusReceiver { + + public static final String ACTION_LASTFMAPI_START = "fm.last.android.metachanged"; + + public static final String ACTION_LASTFMAPI_PAUSERESUME = "fm.last.android.playbackpaused"; + + public static final String ACTION_LASTFMAPI_STOP = "fm.last.android.playbackcomplete"; + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + + switch (action) { + case ACTION_LASTFMAPI_START: + setState(MicroService.State.START); + setTimestamp(System.currentTimeMillis()); + + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + track.setDuration(bundle.getInt("duration")); + // throws on bad data + setTrack(track); + break; + case ACTION_LASTFMAPI_PAUSERESUME: + if (bundle.containsKey("position")) { + setState(MicroService.State.RESUME); + } else { + setState(MicroService.State.PAUSE); + } + setIsSameAsCurrentTrack(); + break; + case ACTION_LASTFMAPI_STOP: + setState(MicroService.State.COMPLETE); + setIsSameAsCurrentTrack(); + break; + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LgOptimus4xReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LgOptimus4xReceiver.java new file mode 100644 index 000000000..74183c9f8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/LgOptimus4xReceiver.java @@ -0,0 +1,98 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +/** + * A BroadcastReceiver for intents sent by the LG Optimus 4X P880 music player + * + * @author kshahar + * @see AbstractPlayStatusReceiver + * @since 1.4.4 + */ +public class LgOptimus4xReceiver extends AbstractPlayStatusReceiver { + + static final String APP_PACKAGE = "com.lge.music"; + + static final String APP_NAME = "LG Music Player"; + + static final String ACTION_LGE_START = "com.lge.music.metachanged"; + + static final String ACTION_LGE_PAUSERESUME = "com.lge.music.playstatechanged"; + + static final String ACTION_LGE_STOP = "com.lge.music.endofplayback"; + + static final String TAG = LgOptimus4xReceiver.class.getSimpleName(); + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) { + + if (ACTION_LGE_STOP.equals(action)) { + setState(MicroService.State.COMPLETE); + setIsSameAsCurrentTrack(); + Log.d(TAG, "Setting state to COMPLETE"); + return; + } + + if (ACTION_LGE_START.equals(action)) { + setState(MicroService.State.START); + Log.d(TAG, "Setting state to START"); + } else if (ACTION_LGE_PAUSERESUME.equals(action)) { + boolean playing = bundle.getBoolean("playing"); + MicroService.State state = playing + ? (MicroService.State.RESUME) + : (MicroService.State.PAUSE); + setState(state); + Log.d(TAG, "Setting state to " + state.toString()); + } + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + + setTimestamp(System.currentTimeMillis()); + + // set duration + int duration = -1; + Object obj = bundle.get("duration"); + if (obj instanceof Long) { + duration = ((Long) obj).intValue(); + } else if (obj instanceof Integer) { + duration = (Integer) obj; + } + if (duration != -1) { + track.setDuration(duration); + } + + // throws on bad data + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MIUIMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MIUIMusicReceiver.java new file mode 100644 index 000000000..b26b4c976 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MIUIMusicReceiver.java @@ -0,0 +1,22 @@ +package org.tomahawk.tomahawk_android.receiver; + +import android.content.Context; +import android.os.Bundle; + +public class MIUIMusicReceiver extends BuiltInMusicAppReceiver { + + static final String APP_PACKAGE = "com.miui.player"; + + static final String ACTION_MIUI_STOP = "com.miui.player.playbackcomplete"; + + public MIUIMusicReceiver() { + super(ACTION_MIUI_STOP, APP_PACKAGE, "MIUI Music Player"); + } + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + super.parseIntent(ctx, action, bundle); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MyTouch4GMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MyTouch4GMusicReceiver.java new file mode 100644 index 000000000..610f9459d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/MyTouch4GMusicReceiver.java @@ -0,0 +1,42 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * Simple Last.fm Scrobbler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simple Last.fm Scrobbler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Simple Last.fm Scrobbler. If not, see . + * + * See http://code.google.com/p/a-simple-lastfm-scrobbler/ for the latest version. + */ + +package org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the myTouch 4G Music Player. + * + * @author tgwizard + * @see BuiltInMusicAppReceiver + * @since 1.3.2 + */ +public class MyTouch4GMusicReceiver extends BuiltInMusicAppReceiver { + + // these first two are untested + public static final String ACTION_MYTOUCH4G_PLAYSTATECHANGED = "com.real.IMP.playstatechanged"; + + public static final String ACTION_MYTOUCH4G_STOP = "com.real.IMP.playbackcomplete"; + + // should work + public static final String ACTION_MYTOUCH4G_METACHANGED = "com.real.IMP.metachanged"; + + public MyTouch4GMusicReceiver() { + super(ACTION_MYTOUCH4G_STOP, "com.real.IMP", "myTouch 4G Music Player"); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProReceiver.java new file mode 100644 index 000000000..c9cbe2635 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProReceiver.java @@ -0,0 +1,72 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +public class PlayerProReceiver extends AbstractPlayStatusReceiver { + + static final String APP_PACKAGE = "com.tbig.playerpro"; + + static final String APP_NAME = "Player Pro"; + + static final String ACTION_PLAYERPRO_METACHANGED = "com.tbig.playerpro.metachanged"; + + static final String ACTION_PLAYERPRO_PLAYSTATECHANGED = "com.tbig.playerpro.playstatechanged"; + + static final String ACTION_PLAYERPRO_PLAYBACKCOMPLETE = "com.tbig.playerpro.playbackcomplete"; + + static final String TAG = PlayerProReceiver.class.getSimpleName(); + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) { + + setTimestamp(System.currentTimeMillis()); + + if (ACTION_PLAYERPRO_PLAYSTATECHANGED.equals(action)) { + if (bundle.getBoolean("playing")) { + setState(MicroService.State.RESUME); + } else { + setState(MicroService.State.PAUSE); + } + } else if (ACTION_PLAYERPRO_METACHANGED.equals(action)) { + if (bundle.getBoolean("playing")) { + setState(MicroService.State.START); + } else { + setState(MicroService.State.PAUSE); + } + } else if (ACTION_PLAYERPRO_PLAYBACKCOMPLETE.equals(action)) { + setState(MicroService.State.COMPLETE); + } + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + track.setDuration(bundle.getInt("duration")); + // throws on bad data + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProTrialReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProTrialReceiver.java new file mode 100644 index 000000000..6c75410c4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/PlayerProTrialReceiver.java @@ -0,0 +1,74 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +public class PlayerProTrialReceiver extends AbstractPlayStatusReceiver { + + static final String APP_PACKAGE = "com.tbig.playerprotrial"; + + static final String APP_NAME = "Player Pro Trial"; + + static final String ACTION_PLAYERPROTRIAL_METACHANGED = "com.tbig.playerprotrial.metachanged"; + + static final String ACTION_PLAYERPROTRIAL_PLAYSTATECHANGED + = "com.tbig.playerprotrial.playstatechanged"; + + static final String ACTION_PLAYERPROTRIAL_PLAYBACKCOMPLETE + = "com.tbig.playerprotrial.playbackcomplete"; + + static final String TAG = PlayerProTrialReceiver.class.getSimpleName(); + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) { + + setTimestamp(System.currentTimeMillis()); + + if (ACTION_PLAYERPROTRIAL_PLAYSTATECHANGED.equals(action)) { + if (bundle.getBoolean("playing")) { + setState(MicroService.State.RESUME); + } else { + setState(MicroService.State.PAUSE); + } + } else if (ACTION_PLAYERPROTRIAL_METACHANGED.equals(action)) { + if (bundle.getBoolean("playing")) { + setState(MicroService.State.START); + } else { + setState(MicroService.State.PAUSE); + } + } else if (ACTION_PLAYERPROTRIAL_PLAYBACKCOMPLETE.equals(action)) { + setState(MicroService.State.COMPLETE); + } + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + track.setDuration(bundle.getInt("duration")); + // throws on bad data + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RdioMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RdioMusicReceiver.java new file mode 100644 index 000000000..2b9daed8c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RdioMusicReceiver.java @@ -0,0 +1,87 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * Simple Last.fm Scrobbler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simple Last.fm Scrobbler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Simple Last.fm Scrobbler. If not, see . + * + * See http://code.google.com/p/a-simple-lastfm-scrobbler/ for the latest version. + */ + +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +/** + * A BroadcastReceiver for intents sent by the Rdio Music Player. + * + * @author tgwizard + * @see BuiltInMusicAppReceiver + * @since 1.3.7 + */ +public class RdioMusicReceiver extends AbstractPlayStatusReceiver { + + static final String TAG = RdioMusicReceiver.class.getSimpleName(); + + static final String APP_PACKAGE = "com.rdio.android.ui"; + + static final String APP_NAME = "Rdio"; + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + + // state, required + boolean isPaused = bundle.getBoolean("isPaused"); + boolean isPlaying = bundle.getBoolean("isPlaying"); + + if (isPlaying) { + setState(MicroService.State.RESUME); + } else if (isPaused) { + setState(MicroService.State.PAUSE); + } else { + setState(MicroService.State.COMPLETE); + } + + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + + setTimestamp(System.currentTimeMillis()); + + long duration = -1; + Object obj = bundle.get("duration"); + if (obj instanceof Integer) { + duration = (Integer) obj; + } + if (obj instanceof Double) { + duration = ((Double) obj).longValue(); + } + if (duration != -1) { + // duration is in milliseconds + track.setDuration(duration); + } + + // throws on bad data + setTrack(track); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RocketPlayerReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RocketPlayerReceiver.java new file mode 100644 index 000000000..af10e2380 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/RocketPlayerReceiver.java @@ -0,0 +1,38 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +public class RocketPlayerReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_ANDROID_PLAYSTATECHANGED + = "com.jrtstudio.AnotherMusicPlayer.playstatechanged"; + + public static final String ACTION_ANDROID_STOP + = "com.jrtstudio.AnotherMusicPlayer.playbackcomplete"; + + public static final String ACTION_ANDROID_METACHANGED + = "com.jrtstudio.AnotherMusicPlayer.metachanged"; + + public static final String PACKAGE_NAME = "com.jrtstudio.AnotherMusicPlayer"; + + public static final String NAME = "Rocket Player"; + + public RocketPlayerReceiver() { + super(ACTION_ANDROID_STOP, PACKAGE_NAME, NAME); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SEMCMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SEMCMusicReceiver.java new file mode 100644 index 000000000..0e8f6fc9d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SEMCMusicReceiver.java @@ -0,0 +1,62 @@ +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +public class SEMCMusicReceiver extends BuiltInMusicAppReceiver { + + static final String APP_PACKAGE = "com.sonyericsson.music"; + + static final String ACTION_SEMC_STOP_LEGACY + = "com.sonyericsson.music.playbackcontrol.ACTION_PLAYBACK_PAUSE"; + + static final String ACTION_SEMC_STOP = "com.sonyericsson.music.playbackcontrol.ACTION_PAUSED"; + + private static final String TAG = SEMCMusicReceiver.class.getSimpleName(); + + public SEMCMusicReceiver() { + super(ACTION_SEMC_STOP, APP_PACKAGE, "Sony Ericsson Music Player"); + } + + @Override + /** + * Checks that the action received is either the one used in the + * newer Sony Xperia devices or that of the previous versions + * of the app. + * + * @param action the received action + * @return true when the received action is a stop action, false otherwise + */ + protected boolean isStopAction(String action) { + return action.equals(ACTION_SEMC_STOP) || action.equals(ACTION_SEMC_STOP_LEGACY); + } + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + Log.d(TAG, "Will read data from SEMC intent"); + + setTimestamp(System.currentTimeMillis()); + + CharSequence ar = bundle.getCharSequence("ARTIST_NAME"); + CharSequence al = bundle.getCharSequence("ALBUM_NAME"); + CharSequence tr = bundle.getCharSequence("TRACK_NAME"); + + if (ar == null || tr == null) { + throw new IllegalArgumentException("null track values"); + } + + Artist artist = Artist.get(ar.toString()); + Album album = null; + if (al != null) { + album = Album.get(al.toString(), artist); + } + Track track = Track.get(tr.toString(), album, artist); + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SLSAPIReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SLSAPIReceiver.java new file mode 100644 index 000000000..a5477f655 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SLSAPIReceiver.java @@ -0,0 +1,120 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * http://code.google.com/p/a-simple-lastfm-scrobbler/ + * + * Copyright 2011 Simple Last.fm Scrobbler Team + * + * 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 org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +/** + * A BroadcastReceiver for the Simple Last.fm Scrobbler API. More info available at the SLS dev page. + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.2.3 + */ +public class SLSAPIReceiver extends AbstractPlayStatusReceiver { + + private static final String TAG = SLSAPIReceiver.class.getSimpleName(); + + public static final String SLS_API_BROADCAST_INTENT = "com.adam.aslfms.notify.playstatechanged"; + + public static final int STATE_START = 0; + + public static final int STATE_RESUME = 1; + + public static final int STATE_PAUSE = 2; + + public static final int STATE_COMPLETE = 3; + + private int getIntFromBundle(Bundle bundle, String key, boolean throwOnFailure) + throws IllegalArgumentException { + long value = -1; + Object obj = bundle.get(key); + + if (obj instanceof Long) { + value = (Long) obj; + } else if (obj instanceof Integer) { + value = (Integer) obj; + } else if (obj instanceof String) { + value = Long.valueOf((String) obj); + } else if (throwOnFailure) { + throw new IllegalArgumentException(key + "not found in intent"); + } + + return (int) value; + } + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + // state, required + int state = getIntFromBundle(bundle, "state", true); + + if (state == STATE_START) { + setState(MicroService.State.START); + } else if (state == STATE_RESUME) { + setState(MicroService.State.RESUME); + } else if (state == STATE_PAUSE) { + setState(MicroService.State.PAUSE); + } else if (state == STATE_COMPLETE) { + setState(MicroService.State.COMPLETE); + } else { + throw new IllegalArgumentException("bad state: " + state); + } + + setTimestamp(System.currentTimeMillis()); + + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + + // duration, required + int duration = getIntFromBundle(bundle, "duration", true); + track.setDuration(duration); + + // tracknr, optional + int tracknr = getIntFromBundle(bundle, "track-number", false); + if (tracknr != -1) { + track.setAlbumPos(tracknr); + } + + // music-brainz id, optional + String mbid = bundle.getString("mbid"); + setMbid(mbid); + + // source, optional (defaults to "P") + String source = bundle.getString("source"); + source = (source == null) ? "P" : source; + setSource(source); + + // throws on bad data + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SamsungMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SamsungMusicReceiver.java new file mode 100644 index 000000000..22d5437b7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SamsungMusicReceiver.java @@ -0,0 +1,44 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * Simple Last.fm Scrobbler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simple Last.fm Scrobbler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Simple Last.fm Scrobbler. If not, see . + * + * See http://code.google.com/p/a-simple-lastfm-scrobbler/ for the latest version. + */ + +package org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the Samsung default Music Player. + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.3.1 + */ +public class SamsungMusicReceiver extends BuiltInMusicAppReceiver { + + public static final String ACTION_SAMSUNG_PLAYSTATECHANGED + = "com.samsung.sec.android.MusicPlayer.playstatechanged"; + + public static final String ACTION_SAMSUNG_STOP + = "com.samsung.sec.android.MusicPlayer.playbackcomplete"; + + public static final String ACTION_SAMSUNG_METACHANGED + = "com.samsung.sec.android.MusicPlayer.metachanged"; + + public SamsungMusicReceiver() { + super(ACTION_SAMSUNG_STOP, "com.samsung.sec.android.MusicPlayer", + "Samsung Music Player"); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/ScrobbleDroidMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/ScrobbleDroidMusicReceiver.java new file mode 100644 index 000000000..b0e248cae --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/ScrobbleDroidMusicReceiver.java @@ -0,0 +1,92 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * Simple Last.fm Scrobbler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simple Last.fm Scrobbler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Simple Last.fm Scrobbler. If not, see . + * + * See http://code.google.com/p/a-simple-lastfm-scrobbler/ for the latest version. + */ + +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +/** + * A BroadcastReceiver for the + * Scrobbler Droid API. New music apps are recommended to use the SLS API instead. + * + * @author tgwizard + * @see AbstractPlayStatusReceiver + * @since 1.2 + */ +public class ScrobbleDroidMusicReceiver extends AbstractPlayStatusReceiver { + + private static final String TAG = ScrobbleDroidMusicReceiver.class.getSimpleName(); + + public static final String SCROBBLE_DROID_MUSIC_STATUS + = "net.jjc1138.android.scrobbler.action.MUSIC_STATUS"; + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) + throws IllegalArgumentException { + + boolean playing = bundle.getBoolean("playing", false); + + if (!playing) { + // if not playing, there is no guarantee the bundle will contain any track info + setState(MicroService.State.UNKNOWN_NONPLAYING); + setIsSameAsCurrentTrack(); + return; + } + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + + String source = bundle.getString("source"); + if (source == null || source.length() > 1) { + source = "P"; + } + setSource(source); + + setTimestamp(System.currentTimeMillis()); + + String mbid = bundle.getString("mb-trackid"); // optional + setMbid(mbid); + + int duration = bundle.getInt("secs", -1); // optional unless source + // is P, but we don't care + if (duration != -1) { + track.setDuration(duration * 1000); + } + + int tnr = bundle.getInt("tracknumber", -1); // optional + if (tnr != -1) { + track.setAlbumPos(tnr); + } + + // we've handled stopping/pausing at the top + setState(MicroService.State.RESUME); + + setTrack(track); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SpotifyReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SpotifyReceiver.java new file mode 100644 index 000000000..813f98c56 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/SpotifyReceiver.java @@ -0,0 +1,68 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.receiver; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.tomahawk_android.services.MicroService; + +import android.content.Context; +import android.os.Bundle; + +public class SpotifyReceiver extends AbstractPlayStatusReceiver { + + static final String APP_PACKAGE = "com.spotify.mobile.android"; + + static final String APP_NAME = "Spotify Android"; + + static final String ACTION_SPOTIFY_METADATACHANGED + = "com.spotify.mobile.android.metadatachanged"; + + static final String ACTION_SPOTIFY_PLAYBACKSTATECHANGED + = "com.spotify.mobile.android.playbackstatechanged"; + + static final String TAG = SpotifyReceiver.class.getSimpleName(); + + @Override + protected void parseIntent(Context ctx, String action, Bundle bundle) { + + if (ACTION_SPOTIFY_PLAYBACKSTATECHANGED.equals(action)) { + if (bundle.getBoolean("playing")) { + setState(MicroService.State.RESUME); + setIsSameAsCurrentTrack(); + } else { + setState(MicroService.State.PAUSE); + setIsSameAsCurrentTrack(); + } + } else if (ACTION_SPOTIFY_METADATACHANGED.equals(action)) { + setState(MicroService.State.START); + Artist artist = Artist.get(bundle.getString("artist")); + Album album = null; + if (bundle.getString("album") != null) { + album = Album.get(bundle.getString("album"), artist); + } + Track track = Track.get(bundle.getString("track"), album, artist); + + setTimestamp(System.currentTimeMillis()); + + // throws on bad data + setTrack(track); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/receiver/WinampMusicReceiver.java b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/WinampMusicReceiver.java new file mode 100644 index 000000000..de32d6652 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/receiver/WinampMusicReceiver.java @@ -0,0 +1,44 @@ +/** + * This file is part of Simple Last.fm Scrobbler. + * + * Simple Last.fm Scrobbler is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simple Last.fm Scrobbler is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Simple Last.fm Scrobbler. If not, see . + * + * See http://code.google.com/p/a-simple-lastfm-scrobbler/ for the latest version. + */ + +package org.tomahawk.tomahawk_android.receiver; + +/** + * A BroadcastReceiver for intents sent by the Winamp Music Player. + * + * @author tgwizard + * @see BuiltInMusicAppReceiver + * @since 1.3.2 + */ +public class WinampMusicReceiver extends BuiltInMusicAppReceiver { + + static final String TAG = WinampMusicReceiver.class.getSimpleName(); + + public static final String ACTION_WINAMP_START = "com.nullsoft.winamp.metachanged"; + + public static final String ACTION_WINAMP_PAUSERESUME = "com.nullsoft.winamp.playstatechanged"; + + // doesn't seem to work + public static final String ACTION_WINAMP_STOP = "com.nullsoft.winamp.playbackcomplete"; + + public WinampMusicReceiver() { + super(ACTION_WINAMP_STOP, "com.nullsoft.winamp", "Winamp"); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/services/MicroService.java b/app/src/main/java/org/tomahawk/tomahawk_android/services/MicroService.java new file mode 100644 index 000000000..be141a61e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/services/MicroService.java @@ -0,0 +1,249 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.services; + +import org.electricwisdom.unifiedremotemetadataprovider.media.RemoteMetadataProvider; +import org.electricwisdom.unifiedremotemetadataprovider.media.enums.PlayState; +import org.electricwisdom.unifiedremotemetadataprovider.media.enums.RemoteControlFeature; +import org.electricwisdom.unifiedremotemetadataprovider.media.listeners.OnArtworkChangeListener; +import org.electricwisdom.unifiedremotemetadataprovider.media.listeners.OnMetadataChangeListener; +import org.electricwisdom.unifiedremotemetadataprovider.media.listeners.OnPlaybackStateChangeListener; +import org.electricwisdom.unifiedremotemetadataprovider.media.listeners.OnRemoteControlFeaturesChangeListener; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.text.TextUtils; +import android.util.Log; + +import java.util.List; + +public class MicroService extends Service { + + private static final String TAG = MicroService.class.getSimpleName(); + + public static final String ACTION_PLAYSTATECHANGED + = "org.tomahawk.tomahawk_android.playstatechanged"; + + public static final String EXTRA_TRACKKEY = "org.tomahawk.tomahawk_android.track_key"; + + public static final String EXTRA_STATE = "org.tomahawk.tomahawk_android.extra_state"; + + public static final String EXTRA_TIMESTAMP = "org.tomahawk.tomahawk_android.extra_timestamp"; + + public static final String EXTRA_SOURCE = "org.tomahawk.tomahawk_android.extra_source"; + + public static final String EXTRA_MBID = "org.tomahawk.tomahawk_android.extra_mbid"; + + public static final String EXTRA_IS_SAME_AS_CURRENT_TRACK + = "org.tomahawk.tomahawk_android.is_same_as_current_track"; + + public enum State { + START, RESUME, PAUSE, COMPLETE, PLAYLIST_FINISHED, UNKNOWN_NONPLAYING + } + + private static Track sCurrentTrack = null; + + private RemoteMetadataProvider mMetadataProvider; + + private MicroServiceBroadcastReceiver mMicroServiceBroadcastReceiver; + + /** + * Handles incoming broadcasts + */ + private class MicroServiceBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_HEADSET_PLUG.equals(intent.getAction()) && intent + .hasExtra("state") && intent.getIntExtra("state", 0) == 1) { + Log.d(TAG, "Headset has been plugged in"); + + if (PreferenceUtils.getBoolean(PreferenceUtils.PLUG_IN_TO_PLAY)) { + //resume playback, if user has set the "resume on headset plugin" preference + context.startService(new Intent(PlaybackService.ACTION_PLAY, null, context, + PlaybackService.class)); + } + } + } + } + + @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + super.onCreate(); + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + //Acquiring instance of RemoteMetadataProvider + mMetadataProvider = RemoteMetadataProvider.getInstance(this); + //setting up metadata listener + mMetadataProvider.setOnMetadataChangeListener(new OnMetadataChangeListener() { + @Override + public void onMetadataChanged(String artistName, String trackName, + String albumName, + String albumArtistName, long duration) { + Log.d(TAG, "onMetadataChanged"); + scrobbleTrack(trackName, artistName, albumName, albumArtistName); + } + }); + + //setting up artwork listener + mMetadataProvider.setOnArtworkChangeListener(new OnArtworkChangeListener() { + @Override + public void onArtworkChanged(Bitmap artwork) { + Log.d(TAG, "onArtworkChanged"); + } + }); + + //setting up remote control flags listener + mMetadataProvider.setOnRemoteControlFeaturesChangeListener( + new OnRemoteControlFeaturesChangeListener() { + @Override + public void onFeaturesChanged(List usesFeatures) { + Log.d(TAG, "onFeaturesChanged"); + } + } + ); + + //setting up playback state change listener + mMetadataProvider + .setOnPlaybackStateChangeListener(new OnPlaybackStateChangeListener() { + @Override + public void onPlaybackStateChanged(PlayState playbackState) { + Log.d(TAG, "onPlaybackStateChanged"); + } + }); + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + mMetadataProvider.acquireRemoteControls(); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + mMetadataProvider.acquireRemoteControls(256, 256); + } + } + + mMicroServiceBroadcastReceiver = new MicroServiceBroadcastReceiver(); + registerReceiver(mMicroServiceBroadcastReceiver, + new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent i, int flags, int startId) { + if (i != null) { + String action = i.getAction(); + Bundle extras = i.getExtras(); + if (ACTION_PLAYSTATECHANGED.equals(action)) { + if (extras != null) { + MicroService.State state = MicroService.State + .valueOf(extras.getString(EXTRA_STATE)); + + Track track = Track.getByKey(extras.getString(EXTRA_TRACKKEY)); + boolean isSameAsCurrentTrack = extras + .containsKey(EXTRA_IS_SAME_AS_CURRENT_TRACK); + String source = extras.getString(EXTRA_SOURCE); + if (track != null || isSameAsCurrentTrack) { + onPlayStateChanged(track, state, isSameAsCurrentTrack, source); + } + } + } + } + return Service.START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + unregisterReceiver(mMicroServiceBroadcastReceiver); + mMicroServiceBroadcastReceiver = null; + + Log.d(TAG, "MicroService has been destroyed"); + } + + private synchronized void onPlayStateChanged(Track track, MicroService.State state, + boolean isSameAsCurrentTrack, String source) { + if (isSameAsCurrentTrack) { + // this only happens for apps implementing Scrobble Droid's API + Log.d(TAG, "Got a SAME_AS_CURRENT track"); + if (sCurrentTrack != null) { + track = sCurrentTrack; + } else { + Log.e(TAG, "Got a SAME_AS_CURRENT track, but current was null!"); + return; + } + } + if (State.RESUME.equals(state) || State.START.equals(state)) { + scrobbleTrack(track.getName(), track.getArtist().getName(), track.getAlbum().getName(), + null); + } + } + + public static void scrobbleTrack(String trackName, String artistName, String albumName, + String albumArtistName) { + boolean scrobbleEverything = PreferenceUtils.getBoolean(PreferenceUtils.SCROBBLE_EVERYTHING) + || Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + if (scrobbleEverything && !TextUtils.isEmpty(trackName) && (!TextUtils.isEmpty(artistName) + || !TextUtils.isEmpty(albumArtistName) || !TextUtils.isEmpty(albumName))) { + Artist artist; + Album album; + if (!TextUtils.isEmpty(artistName)) { + artist = Artist.get(artistName); + album = Album.get(albumName, artist); + } else if (!TextUtils.isEmpty(albumArtistName)) { + artist = Artist.get(albumArtistName); + album = Album.get(albumName, artist); + } else { + // Since the artistName is empty and the albumName isn't, we just assume that + // something got switched up. So we use the albumName as the artistName instead. + artist = Artist.get(albumName); + album = Album.get(null, artist); + } + Track track = Track.get(trackName, album, artist); + if (sCurrentTrack != track) { + sCurrentTrack = track; + AuthenticatorUtils utils = AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + InfoSystem.get().sendNowPlayingPostStruct(utils, Query.get(track, false)); + Log.d(TAG, "Scrobbling " + track); + } + } else { + Log.d(TAG, "Didn't scrobble track: '" + trackName + "' - '" + + artistName + "' - '" + albumName + "' - '" + albumArtistName); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/services/PlaybackService.java b/app/src/main/java/org/tomahawk/tomahawk_android/services/PlaybackService.java new file mode 100644 index 000000000..c8543f998 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/services/PlaybackService.java @@ -0,0 +1,1280 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Christopher Reichert + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.services; + +import org.jdeferred.DoneCallback; +import org.jdeferred.FailCallback; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; +import org.tomahawk.tomahawk_android.listeners.MediaImageLoadedListener; +import org.tomahawk.tomahawk_android.mediaplayers.AndroidMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.DeezerMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.PluginMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.SpotifyMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.TomahawkMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.TomahawkMediaPlayerCallback; +import org.tomahawk.tomahawk_android.mediaplayers.VLCMediaPlayer; +import org.tomahawk.tomahawk_android.utils.DelayedHandler; +import org.tomahawk.tomahawk_android.utils.IdGenerator; +import org.tomahawk.tomahawk_android.utils.MediaBrowserHelper; +import org.tomahawk.tomahawk_android.utils.MediaImageHelper; +import org.tomahawk.tomahawk_android.utils.MediaNotification; +import org.tomahawk.tomahawk_android.utils.MediaPlayIntentHandler; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.utils.ThreadManager; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaButtonReceiver; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.util.SparseArrayCompat; +import android.util.Log; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +/** + * This {@link Service} handles all playback related processes. + */ +public class PlaybackService extends MediaBrowserServiceCompat { + + private static final String TAG = PlaybackService.class.getSimpleName(); + + public static final String ACTION_PLAY + = "org.tomahawk.tomahawk_android.ACTION_PLAY"; + + public static final String ACTION_STOP_NOTIFICATION + = "org.tomahawk.tomahawk_android.STOP_NOTIFICATION"; + + public static final String ACTION_DELETE_ENTRY_IN_QUEUE + = "org.tomahawk.tomahawk_android.DELETE_ENTRY_IN_QUEUE"; + + public static final String ACTION_ADD_QUERY_TO_QUEUE + = "org.tomahawk.tomahawk_android.ADD_QUERY_TO_QUEUE"; + + public static final String ACTION_ADD_QUERIES_TO_QUEUE + = "org.tomahawk.tomahawk_android.ADD_QUERIES_TO_QUEUE"; + + public static final String ACTION_SET_SHUFFLE_MODE + = "org.tomahawk.tomahawk_android.SET_SHUFFLE_MODE"; + + public static final String ACTION_SET_REPEAT_MODE + = "org.tomahawk.tomahawk_android.SET_REPEAT_MODE"; + + public static final String EXTRAS_KEY_PLAYBACKMANAGER + = "org.tomahawk.tomahawk_android.PLAYBACKMANAGER"; + + public static final String EXTRAS_KEY_REPEAT_MODE + = "org.tomahawk.tomahawk_android.REPEAT_MODE"; + + public static final String EXTRAS_KEY_SHUFFLE_MODE + = "org.tomahawk.tomahawk_android.SHUFFLE_MODE"; + + // we don't have audio focus, and can't duck (play at a low volume) + private static final int AUDIO_NO_FOCUS_NO_DUCK = 0; + + // we don't have focus, but can duck (play at a low volume) + private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1; + + // we have full audio focus + private static final int AUDIO_FOCUSED = 2; + + private boolean mIsDestroyed; + + private int mPlayState = PlaybackStateCompat.STATE_NONE; + + private boolean mIsPreparing = false; + + private static final int DELAY_SCROBBLE = 15000; + + private static final int DELAY_UNBIND_PLUGINSERVICES = 1800000; + + private static final int DELAY_SUICIDE = 1800000; + + private final Set mCorrespondingQueries + = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final ConcurrentHashMap mCorrespondingRequestIds + = new ConcurrentHashMap<>(); + + private final Map> mStationQueries = new ConcurrentHashMap<>(); + + private PlaybackManager mPlaybackManager; + + private TomahawkMediaPlayer mCurrentMediaPlayer; + + private final Map mMediaPlayers = new HashMap<>(); + + private MediaNotification mNotification; + + private MediaSessionCompat mMediaSession; + + private Handler mCallbackHandler; + + private MediaBrowserHelper mMediaBrowserHelper; + + private SparseArrayCompat mQueueMap = new SparseArrayCompat<>(); + + private boolean mPlayOnFocusGain; + + private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; + + private AudioManager mAudioManager; + + private AudioManager.OnAudioFocusChangeListener mFocusChangeListener + = new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) { + Log.d(TAG, "onAudioFocusChange. focusChange= " + focusChange); + if (mIsDestroyed) { + Log.d(TAG, "onAudioFocusChange. Ignoring because PlaybackService is destroyed"); + return; + } + if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + // We have gained focus + mAudioFocus = AUDIO_FOCUSED; + if (mPlayState == PlaybackStateCompat.STATE_PAUSED) { + play(true); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + // We have lost focus + mAudioFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK; + if (mPlayState == PlaybackStateCompat.STATE_PLAYING) { + pause(true); + } + } else { + Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); + } + } + }; + + private MediaImageLoadedListener mMediaImageLoadedListener = new MediaImageLoadedListener() { + @Override + public void onMediaImageLoaded() { + if (mMediaSession != null) { + MediaMetadataCompat metadata = buildMetadata(); + synchronized (this) { + mMediaSession.setMetadata(metadata); + } + } + } + }; + + private MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { + + /** + * Override to handle requests to begin playback. + */ + @Override + public void onPlay() { + play(false); + } + + /** + * Override to handle requests to pause playback. + */ + @Override + public void onPause() { + pause(false); + } + + /** + * Override to handle requests to begin playback from a search query. An + * empty query indicates that the app may play any music. The + * implementation should attempt to make a smart choice about what to + * play. + */ + public void onPlayFromSearch(String query, Bundle extras) { + Log.d(TAG, "onPlayFromSearch: " + query + ", " + extras); + MediaPlayIntentHandler intentHandler = new MediaPlayIntentHandler( + mMediaSession.getController().getTransportControls(), mPlaybackManager); + intentHandler.mediaPlayFromSearch(extras); + } + + /** + * Override to handle requests to play a specific mediaId that was + * provided by your app. + */ + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + if (mMediaSession == null) { + Log.e(TAG, "onPlayFromMediaId failed - mMediaSession == null!"); + return; + } + mMediaBrowserHelper.onPlayFromMediaId(mMediaSession, mPlaybackManager, mediaId, extras); + } + + /** + * Override to handle requests to play an item with a given id from the + * play queue. + */ + public void onSkipToQueueItem(long id) { + Log.d(TAG, "Skipping to queue item with id " + id); + PlaylistEntry entry = mQueueMap.get((int) id); + mPlaybackManager.setCurrentEntry(entry); + } + + /** + * Override to handle requests to skip to the next media item. + */ + @Override + public void onSkipToNext() { + Log.d(TAG, "next"); + int counter = 0; + PlaylistEntry entry = mPlaybackManager.getCurrentEntry(); + while ((entry = mPlaybackManager.getNextEntry(entry)) != null + && counter++ < mPlaybackManager.getPlaybackListSize()) { + if (entry.getQuery().isPlayable()) { + mPlaybackManager.setCurrentEntry(entry); + break; + } + } + } + + /** + * Override to handle requests to skip to the previous media item. + */ + @Override + public void onSkipToPrevious() { + Log.d(TAG, "previous"); + int counter = 0; + PlaylistEntry entry = mPlaybackManager.getCurrentEntry(); + while ((entry = mPlaybackManager.getPreviousEntry(entry)) != null + && counter++ < mPlaybackManager.getPlaybackListSize()) { + if (entry.getQuery().isPlayable()) { + mPlaybackManager.setCurrentEntry(entry); + break; + } + } + } + + /** + * Override to handle requests to fast forward. + */ + @Override + public void onFastForward() { + Log.d(TAG, "fastForward"); + long duration = mPlaybackManager.getCurrentTrack().getDuration(); + long newPos = Math.min(duration, Math.max(0, getPlaybackPosition() + 10000)); + onSeekTo(newPos); + } + + /** + * Override to handle requests to rewind. + */ + @Override + public void onRewind() { + Log.d(TAG, "rewind"); + long duration = mPlaybackManager.getCurrentTrack().getDuration(); + long newPos = Math.min(duration, Math.max(0, getPlaybackPosition() - 10000)); + onSeekTo(newPos); + } + + /** + * Override to handle requests to stop playback. + */ + @Override + public void onStop() { + onPause(); + } + + /** + * Override to handle requests to seek to a specific position in ms. + * + * @param pos New position to move to, in milliseconds. + */ + @Override + public void onSeekTo(final long pos) { + Log.d(TAG, "seekTo " + pos); + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { + final TomahawkMediaPlayer mp = + mMediaPlayers.get(currentQuery.getMediaPlayerClass()); + Runnable r = new Runnable() { + @Override + public void run() { + if (mp.isPrepared(currentQuery)) { + mp.seekTo(pos); + updateMediaPlayState(); + } + } + }; + ThreadManager.get().executePlayback(mp, r); + } + } + + /** + * Override to handle the item being rated. + */ + @Override + public void onSetRating(RatingCompat rating) { + if (rating.getRatingStyle() == RatingCompat.RATING_HEART + && mPlaybackManager.getCurrentQuery() != null) { + CollectionManager.get().setLovedItem( + mPlaybackManager.getCurrentQuery(), rating.hasHeart()); + mPlaybackManagerCallback.onCurrentEntryChanged(); + } else if (rating.getRatingStyle() == RatingCompat.RATING_THUMB_UP_DOWN + && mPlaybackManager.getCurrentQuery() != null) { + CollectionManager.get().setLovedItem( + mPlaybackManager.getCurrentQuery(), rating.isThumbUp()); + mPlaybackManagerCallback.onCurrentEntryChanged(); + } + } + + @Override + public void onCustomAction(String action, Bundle extras) { + if (ACTION_STOP_NOTIFICATION.equals(action)) { + mNotification.stopNotification(); + } else if (ACTION_DELETE_ENTRY_IN_QUEUE.equals(action)) { + PlaylistEntry entry = + PlaylistEntry.getByKey(extras.getString(TomahawkFragment.PLAYLISTENTRY)); + mPlaybackManager.deleteFromQueue(entry); + } else if (ACTION_ADD_QUERY_TO_QUEUE.equals(action)) { + Query query = Query.getByKey(extras.getString(TomahawkFragment.QUERY)); + mPlaybackManager.addToQueue(query); + } else if (ACTION_ADD_QUERIES_TO_QUEUE.equals(action)) { + List queryKeys = extras.getStringArrayList(TomahawkFragment.QUERYARRAY); + List queries = new ArrayList<>(); + for (String queryKey : queryKeys) { + queries.add(Query.getByKey(queryKey)); + } + mPlaybackManager.addToQueue(queries); + } else if (ACTION_SET_SHUFFLE_MODE.equals(action)) { + int shuffleMode = extras.getInt(EXTRAS_KEY_SHUFFLE_MODE); + Log.d(TAG, "setShuffleMode to " + shuffleMode); + mPlaybackManager.setShuffleMode(shuffleMode); + } else if (ACTION_SET_REPEAT_MODE.equals(action)) { + int repeatMode = extras.getInt(EXTRAS_KEY_REPEAT_MODE); + Log.d(TAG, "setRepeatMode to " + repeatMode); + mPlaybackManager.setRepeatMode(repeatMode); + } + } + }; + + public void play(boolean onAudioFocusGain) { + Log.d(TAG, "play"); + if (onAudioFocusGain && !mPlayOnFocusGain) { + return; + } + if (mPlaybackManager.getCurrentQuery() != null) { + if (mAudioBecomingNoisyReceiver == null) { + mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); + registerReceiver(mAudioBecomingNoisyReceiver, + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + } + mSuicideHandler.stop(); + mSuicideHandler.reset(); + mPluginServiceKillHandler.stop(); + mPluginServiceKillHandler.reset(); + mScrobbleHandler.start(); + mPlayState = PlaybackStateCompat.STATE_PLAYING; + handlePlayState(); + + tryToGetAudioFocus(); + updateMediaPlayState(); + mNotification.startNotification(); + } + } + + public void pause(boolean onAudioFocusLost) { + Log.d(TAG, "pause"); + mPlayOnFocusGain = onAudioFocusLost; + if (mAudioBecomingNoisyReceiver != null) { + unregisterReceiver(mAudioBecomingNoisyReceiver); + mAudioBecomingNoisyReceiver = null; + } + mSuicideHandler.start(); + mPluginServiceKillHandler.start(); + mScrobbleHandler.stop(); + mPlayState = PlaybackStateCompat.STATE_PAUSED; + handlePlayState(); + updateMediaPlayState(); + } + + private PlaybackManager.Callback mPlaybackManagerCallback = new PlaybackManager.Callback() { + @Override + public synchronized void onPlaylistChanged() { + Playlist playlist = mPlaybackManager.getPlaylist(); + Log.d(TAG, "Playlist has changed to: " + playlist); + if (playlist instanceof StationPlaylist) { + StationPlaylist stationPlaylist = (StationPlaylist) playlist; + stationPlaylist.setPlayedTimeStamp(System.currentTimeMillis()); + if (stationPlaylist.getPlaylist() == null) { + DatabaseHelper.get().storeStation(stationPlaylist); + } + if (!mPlaybackManager.hasNextEntry(mPlaybackManager.getNextEntry())) { + // there's no track after the next one, + // so we should fill the station with some new tracks + if (mPlaybackManager.getCurrentEntry() == null) { + mIsPreparing = true; + } + updateMediaPlayState(); + fillStation(stationPlaylist); + } + } + onCurrentEntryChanged(); + } + + @Override + public synchronized void onCurrentEntryChanged() { + Log.d(TAG, "Current entry has changed to: " + mPlaybackManager.getCurrentEntry()); + if (mPlaybackManager.getCurrentEntry() == null) { + mNotification.stopNotification(); + } + handlePlayState(); + Playlist playlist = mPlaybackManager.getPlaylist(); + if (playlist instanceof StationPlaylist) { + if (!mPlaybackManager.hasNextEntry(mPlaybackManager.getNextEntry())) { + // there's no track after the next one, + // so we should fill the station with some new tracks + fillStation((StationPlaylist) playlist); + } + } + resolveProximalQueries(); + updateMediaMetadata(); + updateMediaQueue(); + updateMediaPlayState(); + } + + @Override + public synchronized void onShuffleModeChanged() { + updateMediaMetadata(); + updateMediaQueue(); + updateMediaPlayState(); + } + + @Override + public synchronized void onRepeatModeChanged() { + updateMediaMetadata(); + updateMediaQueue(); + updateMediaPlayState(); + } + }; + + private void fillStation(final StationPlaylist stationPlaylist) { + Promise, Throwable, Void> promise = stationPlaylist.fillPlaylist(10); + if (promise != null) { + Log.d(TAG, "filling " + stationPlaylist); + promise.done(new DoneCallback>() { + @Override + public void onDone(List result) { + Log.d(TAG, "found " + result.size() + " candidates to fill " + stationPlaylist); + for (Query query : result) { + mCorrespondingQueries.add(query); + if (!mStationQueries.containsKey(stationPlaylist)) { + Set querySet = Collections.newSetFromMap( + new ConcurrentHashMap()); + mStationQueries.put(stationPlaylist, querySet); + } + mStationQueries.get(stationPlaylist).add(query); + PipeLine.get().resolve(query); + } + } + }); + promise.fail(new FailCallback() { + @Override + public void onFail(final Throwable result) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(TomahawkApp.getContext(), result.getMessage(), + Toast.LENGTH_LONG).show(); + } + }); + } + }); + } + } + + private AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; + + private class AudioBecomingNoisyReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { + // AudioManager tells us that the sound will be played through the speaker + Log.d(TAG, "Action audio becoming noisy, pausing ..."); + // So we stop playback, if needed + mMediaSession.getController().getTransportControls().pause(); + } + } + } + + private PowerManager.WakeLock mWakeLock; + + private RemoteControllerConnection mRemoteControllerConnection; + + private static class RemoteControllerConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "Connected to RemoteControllerService!"); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.e(TAG, "RemoteControllerService has crashed :("); + } + } + + private SuicideHandler mSuicideHandler = new SuicideHandler(this); + + // Stops this service if it doesn't have any bound services + private static class SuicideHandler extends DelayedHandler { + + public SuicideHandler(PlaybackService service) { + super(service, DELAY_SUICIDE); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + Log.d(TAG, "Killtimer called stopSelf() on me"); + getReferencedObject().stopSelf(); + } + } + } + + private PluginServiceKillHandler mPluginServiceKillHandler = new PluginServiceKillHandler(this); + + private static class PluginServiceKillHandler extends DelayedHandler { + + public PluginServiceKillHandler(PlaybackService service) { + super(service, DELAY_UNBIND_PLUGINSERVICES); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + getReferencedObject().unbindPluginServices(); + } + } + } + + private ScrobbleHandler mScrobbleHandler = new ScrobbleHandler(this); + + private static class ScrobbleHandler extends DelayedHandler { + + public ScrobbleHandler(PlaybackService service) { + super(service, DELAY_SCROBBLE); + } + + @Override + public void handleMessage(Message msg) { + if (getReferencedObject() != null) { + Log.d(TAG, "Scrobbling delay has passed. Scrobbling..."); + if (getReferencedObject().mPlaybackManager.getCurrentQuery() != null) { + InfoSystem.get().sendNowPlayingPostStruct( + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET), + getReferencedObject().mPlaybackManager.getCurrentQuery()); + } + } + } + } + + private TomahawkMediaPlayerCallback mMediaPlayerCallback = new TomahawkMediaPlayerCallback() { + @Override + public void onPrepared(TomahawkMediaPlayer mediaPlayer, Query query) { + if (mediaPlayer != mCurrentMediaPlayer) { + Log.d(TAG, + "Ignoring onPrepared call, because it hasn't been invoked by mCurrentMediaPlayer"); + return; + } + if (query != null && query == mPlaybackManager.getCurrentQuery()) { + Log.d(TAG, mediaPlayer + " successfully prepared the track " + + mPlaybackManager.getCurrentQuery() + " resolved by " + + mPlaybackManager.getCurrentQuery() + .getPreferredTrackResult().getResolvedBy().getId()); + mIsPreparing = false; + updateMediaPlayState(); + mScrobbleHandler.reset(); + handlePlayState(); + } else { + String queryInfo; + if (query != null) { + queryInfo = mPlaybackManager.getCurrentQuery() + " resolved by " + + mPlaybackManager.getCurrentQuery() + .getPreferredTrackResult().getResolvedBy().getId(); + } else { + queryInfo = "null"; + } + Log.e(TAG, "onPrepared received for an unexpected Query: " + queryInfo); + } + } + + @Override + public void onCompletion(TomahawkMediaPlayer mediaPlayer, Query query) { + if (mediaPlayer != mCurrentMediaPlayer) { + Log.d(TAG, + "Ignoring onCompletion call, because it hasn't been invoked by mCurrentMediaPlayer"); + return; + } + if (mMediaSession == null) { + Log.e(TAG, "onCompletion failed - mMediaSession == null!"); + return; + } + if (query != null && query == mPlaybackManager.getCurrentQuery()) { + Log.d(TAG, "onCompletion - mediaPlayer: " + mediaPlayer + ", query: " + query); + if (mPlaybackManager.hasNextEntry()) { + mMediaSession.getController().getTransportControls().skipToNext(); + } else { + mMediaSession.getController().getTransportControls().pause(); + } + } + } + + @Override + public void onError(TomahawkMediaPlayer mediaPlayer, final String message) { + Log.d(TAG, "onError - mediaPlayer: " + mediaPlayer + ", message: " + message); + if (mediaPlayer != mCurrentMediaPlayer) { + Log.d(TAG, + "Ignoring onError call, because it hasn't been invoked by mCurrentMediaPlayer"); + return; + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(TomahawkApp.getContext(), message, Toast.LENGTH_LONG).show(); + } + }); + giveUpAudioFocus(); + if (mMediaSession == null) { + Log.e(TAG, "onError failed - mMediaSession == null!"); + return; + } + if (mPlaybackManager.hasNextEntry()) { + mMediaSession.getController().getTransportControls().skipToNext(); + } else { + mMediaSession.getController().getTransportControls().pause(); + } + } + }; + + @SuppressWarnings("unused") + public void onEventAsync(PipeLine.ResultsEvent event) { + Playlist playlist = mPlaybackManager.getPlaylist(); + if (playlist instanceof StationPlaylist && event.mQuery.isPlayable() + && mStationQueries.containsKey(playlist) + && mStationQueries.get(playlist).remove(event.mQuery)) { + boolean wasNull = mPlaybackManager.getCurrentEntry() == null; + mPlaybackManager.addToPlaylist(event.mQuery); + if (wasNull) { + if (mMediaSession == null) { + Log.e(TAG, + "onEventAsync(PipeLine.ResultsEvent event) failed - mMediaSession == null!"); + } else { + mMediaSession.getController().getTransportControls().play(); + } + } + } + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null && currentQuery == event.mQuery) { + mPlaybackManagerCallback.onCurrentEntryChanged(); + Runnable r = new Runnable() { + @Override + public void run() { + if (mCurrentMediaPlayer == null + || !(mCurrentMediaPlayer.isPrepared(currentQuery) + || mCurrentMediaPlayer.isPreparing(currentQuery))) { + handlePlayState(); + } + } + }; + ThreadManager.get().executePlayback(mCurrentMediaPlayer, r); + } + } + + @SuppressWarnings("unused") + public void onEventAsync(InfoSystem.ResultsEvent event) { + Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null && currentQuery.getCacheKey() + .equals(mCorrespondingRequestIds.get(event.mInfoRequestData.getRequestId()))) { + mPlaybackManagerCallback.onCurrentEntryChanged(); + } + } + + @SuppressWarnings("unused") + public void onEventAsync(CollectionManager.UpdatedEvent event) { + Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (event.mUpdatedItemIds != null && currentQuery != null + && event.mUpdatedItemIds.contains(currentQuery.getCacheKey())) { + mPlaybackManagerCallback.onCurrentEntryChanged(); + } + } + + public static class RequestServiceBindingEvent { + + private ServiceConnection mConnection; + + private String mServicePackageName; + + public RequestServiceBindingEvent(ServiceConnection connection, String servicePackageName) { + mConnection = connection; + mServicePackageName = servicePackageName; + } + } + + @SuppressWarnings("unused") + public void onEvent(RequestServiceBindingEvent event) { + Intent intent = new Intent(event.mServicePackageName + ".BindToService"); + intent.setPackage(event.mServicePackageName); + bindService(intent, event.mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + public void onCreate() { + super.onCreate(); + + EventBus.getDefault().register(this); + + PipeLine.get(); + + mMediaBrowserHelper = new MediaBrowserHelper(this); + + mMediaPlayers.put(AndroidMediaPlayer.class, new AndroidMediaPlayer()); + mMediaPlayers.put(VLCMediaPlayer.class, new VLCMediaPlayer()); + mMediaPlayers.put(DeezerMediaPlayer.class, new DeezerMediaPlayer()); + mMediaPlayers.put(SpotifyMediaPlayer.class, new SpotifyMediaPlayer()); + + startService(new Intent(this, MicroService.class)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + mRemoteControllerConnection = new RemoteControllerConnection(); + bindService(new Intent(this, RemoteControllerService.class), + mRemoteControllerConnection, Context.BIND_AUTO_CREATE); + } + + // Initialize WakeLock + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mPlaybackManager = PlaybackManager.get(IdGenerator.getSessionUniqueStringId()); + mPlaybackManager.setCallback(mPlaybackManagerCallback); + + initMediaSession(); + + try { + mNotification = new MediaNotification(this); + } catch (RemoteException e) { + Log.e(TAG, "Could not connect to media controller: ", e); + } + + Log.d(TAG, "PlaybackService has been created"); + } + + private void initMediaSession() { + ComponentName componentName = new ComponentName(this, MediaButtonReceiver.class); + mMediaSession = new MediaSessionCompat( + getApplicationContext(), "Tomahawk", componentName, null); + mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | + MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + Intent intent = new Intent(PlaybackService.this, TomahawkMainActivity.class); + intent.setAction(TomahawkMainActivity.SHOW_PLAYBACKFRAGMENT_ON_STARTUP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(PlaybackService.this, 0, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + mMediaSession.setSessionActivity(pendingIntent); + HandlerThread thread = new HandlerThread("playbackservice_callback"); + thread.start(); + mCallbackHandler = new Handler(thread.getLooper()); + mMediaSession.setCallback(mMediaSessionCallback, mCallbackHandler); + mMediaSession.setRatingType(RatingCompat.RATING_HEART); + Bundle extras = new Bundle(); + extras.putString(EXTRAS_KEY_PLAYBACKMANAGER, mPlaybackManager.getId()); + mMediaSession.setExtras(extras); + updateMediaPlayState(); + setSessionToken(mMediaSession.getSessionToken()); + MediaImageHelper.get().addListener(mMediaImageLoadedListener); + } + + public Handler getCallbackHandler() { + return mCallbackHandler; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && ACTION_STOP_NOTIFICATION.equals(intent.getAction())) { + mMediaSession.getController().getTransportControls().pause(); + mNotification.stopNotification(); + } else { + MediaButtonReceiver.handleIntent(mMediaSession, intent); + } + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "Client has been bound to PlaybackService"); + return super.onBind(intent); + } + + @Nullable + @Override + public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, + @Nullable Bundle rootHints) { + return mMediaBrowserHelper.onGetRoot(clientPackageName, clientUid, rootHints); + } + + @Override + public void onLoadChildren(@NonNull String parentId, + @NonNull final Result> result) { + mMediaBrowserHelper.onLoadChildren(parentId, result); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.d(TAG, "Client has been unbound from PlaybackService"); + return super.onUnbind(intent); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mIsDestroyed = true; + + EventBus.getDefault().unregister(this); + + giveUpAudioFocus(); + + mPlaybackManager.setCallback(null); + + if (mAudioBecomingNoisyReceiver != null) { + unregisterReceiver(mAudioBecomingNoisyReceiver); + mAudioBecomingNoisyReceiver = null; + } + mScrobbleHandler.stop(); + + mPlayState = PlaybackStateCompat.STATE_PAUSED; + handlePlayState(); + mNotification.stopNotification(); + + releaseAllPlayers(); + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + mWakeLock = null; + mSuicideHandler.stop(); + mSuicideHandler = null; + mPluginServiceKillHandler.stop(); + mPluginServiceKillHandler = null; + if (mMediaSession != null) { + mMediaSession.setCallback(null); + synchronized (this) { + mMediaSession.release(); + } + mMediaSession = null; + } + MediaImageHelper.get().removeListener(mMediaImageLoadedListener); + + if (mRemoteControllerConnection != null) { + unbindService(mRemoteControllerConnection); + } + unbindPluginServices(); + + Log.d(TAG, "PlaybackService has been destroyed"); + } + + private void unbindPluginServices() { + Log.d(TAG, "Unbinding all PluginServices..."); + for (TomahawkMediaPlayer mp : mMediaPlayers.values()) { + if (mp instanceof PluginMediaPlayer) { + PluginMediaPlayer pmp = (PluginMediaPlayer) mp; + if (pmp.isBound()) { + pmp.setService(null); + unbindService(pmp.getServiceConnection()); + } + } + } + } + + /** + * Update the TomahawkMediaPlayer so that it reflects the current playState + */ + private void handlePlayState() { + Log.d(TAG, "handlePlayState"); + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { + final TomahawkMediaPlayer mp = mMediaPlayers.get(currentQuery.getMediaPlayerClass()); + Runnable r = new Runnable() { + @Override + public void run() { + switch (mPlayState) { + case PlaybackStateCompat.STATE_PLAYING: + // The service needs to continue running even after the bound client + // (usually a MediaController) disconnects, otherwise the music playback + // will stop. Calling startService(Intent) will keep the service running + // until it is explicitly killed. + startService(new Intent(getApplicationContext(), + PlaybackService.class)); + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + if (mp.isPreparing(currentQuery) || mp.isPrepared(currentQuery)) { + if (!mp.isPlaying(currentQuery)) { + mp.play(); + } + } else { + prepareCurrentQuery(); + } + break; + case PlaybackStateCompat.STATE_PAUSED: + if (mp.isPlaying(currentQuery) && (mp.isPreparing(currentQuery) + || mp.isPrepared(currentQuery))) { + InfoSystem.get().sendPlaybackEntryPostStruct( + AuthenticatorManager.get().getAuthenticatorUtils( + TomahawkApp.PLUGINNAME_HATCHET)); + mp.pause(); + } + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.release(); + } + break; + } + } + }; + ThreadManager.get().executePlayback(mp, r); + } else { + releaseAllPlayers(); + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.release(); + } + Log.d(TAG, "handlePlayState couldn't do anything, isPreparing: " + mIsPreparing); + } + } + + /** + * This method sets the current track and prepares it for playback. + */ + private void prepareCurrentQuery() { + if (mMediaSession == null) { + Log.e(TAG, "prepareCurrentQuery failed - mMediaSession == null!"); + return; + } + Log.d(TAG, "prepareCurrentQuery"); + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null) { + if (!currentQuery.isPlayable() || currentQuery.getMediaPlayerClass() == null) { + Log.e(TAG, currentQuery + " isn't playable. Skipping to next track"); + mMediaSession.getController().getTransportControls().skipToNext(); + } else { + // Resolve images for current query + if (currentQuery.getImage() == null) { + String requestId = InfoSystem.get().resolve( + currentQuery.getArtist(), false); + if (requestId != null) { + mCorrespondingRequestIds.put(requestId, currentQuery.getCacheKey()); + } + requestId = InfoSystem.get().resolve(currentQuery.getAlbum()); + if (requestId != null) { + mCorrespondingRequestIds.put(requestId, currentQuery.getCacheKey()); + } + } + + mIsPreparing = true; + updateMediaPlayState(); + + TomahawkMediaPlayer mp = mMediaPlayers.get(currentQuery.getMediaPlayerClass()); + if (mCurrentMediaPlayer != null && mCurrentMediaPlayer != mp) { + mCurrentMediaPlayer.release(); + } + mCurrentMediaPlayer = mp; + mp.prepare(currentQuery, mMediaPlayerCallback); + } + } + } + + /** + * Returns the position of playback in the current Track. + */ + private long getPlaybackPosition() { + long position = 0; + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { + position = mMediaPlayers.get(currentQuery.getMediaPlayerClass()).getPosition(); + } + return position; + } + + /** + * Update the playback controls/views which are being shown on the lockscreen + */ + private void updateMediaMetadata() { + Log.d(TAG, "updateMediaMetadata()"); + + if (mMediaSession == null) { + Log.e(TAG, "updateMediaMetadata failed - mMediaSession == null!"); + return; + } + + MediaMetadataCompat metadata = buildMetadata(); + synchronized (this) { + mMediaSession.setActive(true); + mMediaSession.setMetadata(metadata); + } + if (mPlaybackManager.getCurrentQuery() != null) { + Log.d(TAG, "Setting media metadata to: " + mPlaybackManager.getCurrentQuery()); + } else if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + Log.d(TAG, "Setting media metadata to: " + getString(R.string.loading_station) + " " + + mPlaybackManager.getPlaylist().getName()); + } else { + Log.e(TAG, "Wasn't able to set media metadata"); + } + } + + private MediaMetadataCompat buildMetadata() { + final Query currentQuery = mPlaybackManager.getCurrentQuery(); + MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); + + if (currentQuery != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, + mPlaybackManager.getCurrentEntry().getCacheKey()) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, + currentQuery.getArtist().getPrettyName()) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, + currentQuery.getArtist().getPrettyName()) + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, + currentQuery.getPrettyName()) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, + currentQuery.getPreferredTrack().getDuration()) + .putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING, + RatingCompat.newHeartRating( + DatabaseHelper.get().isItemLoved(currentQuery))); + if (!currentQuery.getAlbum().getName().isEmpty()) { + builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, + currentQuery.getAlbum().getPrettyName()); + } + Bitmap bitmap; + if (currentQuery.getImage() != null) { + bitmap = MediaImageHelper.get().getMediaImageCache().get(currentQuery.getImage()); + } else { + bitmap = MediaImageHelper.get().getCachedPlaceHolder(); + } + if (bitmap == null) { + // Image is not in cache yet. We have to fetch it... + MediaImageHelper.get().loadMediaImage(currentQuery.getImage()); + } + if (bitmap != null) { + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap); + } + } else if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, + getString(R.string.loading_station) + " " + + mPlaybackManager.getPlaylist().getName()); + } + return builder.build(); + } + + private void updateMediaPlayState() { + if (mMediaSession == null) { + Log.e(TAG, "updateMediaPlayState failed - mMediaSession == null!"); + return; + } + long actions = 0L; + if (mPlaybackManager.getCurrentQuery() != null) { + actions |= PlaybackStateCompat.ACTION_SET_RATING; + } + if (mPlayState == PlaybackStateCompat.STATE_PLAYING) { + actions |= PlaybackStateCompat.ACTION_PAUSE | + PlaybackStateCompat.ACTION_SEEK_TO | + PlaybackStateCompat.ACTION_FAST_FORWARD | + PlaybackStateCompat.ACTION_REWIND; + } else { + actions |= PlaybackStateCompat.ACTION_PLAY; + } + if (mPlaybackManager.hasNextEntry()) { + actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + } + if (mPlaybackManager.hasPreviousEntry()) { + actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } + Log.d(TAG, "updateMediaPlayState()"); + Bundle extras = new Bundle(); + extras.putInt(EXTRAS_KEY_REPEAT_MODE, mPlaybackManager.getRepeatMode()); + extras.putInt(EXTRAS_KEY_SHUFFLE_MODE, mPlaybackManager.getShuffleMode()); + int playState = mIsPreparing ? PlaybackStateCompat.STATE_BUFFERING : mPlayState; + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() + .setActions(actions) + .setState(playState, getPlaybackPosition(), 1f, SystemClock.elapsedRealtime()) + .setExtras(extras); + if (mPlaybackManager.getPlaylist() != null) { + builder.setActiveQueueItemId(mPlaybackManager.getCurrentIndex()); + } + PlaybackStateCompat playbackStateCompat = builder.build(); + synchronized (this) { + mMediaSession.setPlaybackState(playbackStateCompat); + } + } + + private void updateMediaQueue() { + if (mMediaSession == null) { + Log.e(TAG, "updateMediaQueue failed - mMediaSession == null!"); + return; + } + + List queue = buildQueue(); + synchronized (this) { + mMediaSession.setQueue(queue); + mMediaSession.setQueueTitle(getString(R.string.mediabrowser_queue_title)); + } + } + + private List buildQueue() { + List queue = null; + if (mPlaybackManager.getPlaylist() != null) { + queue = new ArrayList<>(); + int currentIndex = mPlaybackManager.getCurrentIndex(); + for (int i = Math.max(0, currentIndex - 1); + i < Math.min(mPlaybackManager.getPlaybackListSize(), currentIndex + 40); i++) { + PlaylistEntry entry = mPlaybackManager.getPlaybackListEntry(i); + MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder(); + descBuilder.setMediaId(entry.getCacheKey()); + descBuilder.setTitle(entry.getQuery().getPrettyName()); + descBuilder.setSubtitle(entry.getArtist().getPrettyName()); + MediaSessionCompat.QueueItem item = + new MediaSessionCompat.QueueItem(descBuilder.build(), i); + queue.add(item); + mQueueMap.put(i, entry); + } + } + return queue; + } + + private void resolveProximalQueries() { + Set qs = new HashSet<>(); + int start = Math.max(0, mPlaybackManager.getCurrentIndex() - 2); + int end = Math.min(mPlaybackManager.getPlaybackListSize(), + mPlaybackManager.getCurrentIndex() + 10); + for (int i = start; i < end; i++) { + Query q = mPlaybackManager.getPlaybackListEntry(i).getQuery(); + if (!mCorrespondingQueries.contains(q)) { + qs.add(q); + } + } + if (!qs.isEmpty()) { + HashSet queries = PipeLine.get().resolve(qs); + mCorrespondingQueries.addAll(queries); + } + } + + private void setBitrate(final int mode) { + for (final TomahawkMediaPlayer mp : mMediaPlayers.values()) { + Runnable r = new Runnable() { + @Override + public void run() { + mp.setBitrate(mode); + } + }; + ThreadManager.get().executePlayback(mp, r); + } + } + + private void releaseAllPlayers() { + for (final TomahawkMediaPlayer mp : mMediaPlayers.values()) { + Runnable r = new Runnable() { + @Override + public void run() { + mp.release(); + } + }; + ThreadManager.get().executePlayback(mp, r); + } + } + + /** + * Try to get the system audio focus. + */ + private void tryToGetAudioFocus() { + Log.d(TAG, "tryToGetAudioFocus"); + if (mAudioFocus != AUDIO_FOCUSED) { + int result = mAudioManager.requestAudioFocus(mFocusChangeListener, + AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AUDIO_FOCUSED; + } + } + } + + /** + * Give up the audio focus. + */ + private void giveUpAudioFocus() { + Log.d(TAG, "giveUpAudioFocus"); + if (mAudioFocus == AUDIO_FOCUSED) { + if (mAudioManager.abandonAudioFocus(mFocusChangeListener) + == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/services/RemoteControllerService.java b/app/src/main/java/org/tomahawk/tomahawk_android/services/RemoteControllerService.java new file mode 100644 index 000000000..8e493f427 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/services/RemoteControllerService.java @@ -0,0 +1,297 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.services; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.MediaMetadataRetriever; +import android.media.RemoteController; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.os.Build; +import android.os.IBinder; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.view.KeyEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Service to fetch all metadata from other media player apps. Does nothing if not run on Kitkat. + * Compat code is included in MicroService instead. + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public class RemoteControllerService extends NotificationListenerService + implements RemoteController.OnClientUpdateListener { + + private static final String TAG = RemoteControllerService.class.getSimpleName(); + + //dimensions in pixels for artwork + private static final int BITMAP_HEIGHT = 1024; + + private static final int BITMAP_WIDTH = 1024; + + private RemoteController mRemoteController; + + private List mActiveSessions; + + private MediaController.Callback mSessionCallback; + + private MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener; + + @Override + public IBinder onBind(Intent intent) { + if ("android.service.notification.NotificationListenerService".equals(intent.getAction())) { + setRemoteControllerEnabled(); + } + return super.onBind(intent); + } + + /** + * Enables the RemoteController thus allowing us to receive metadata updates. + */ + public void setRemoteControllerEnabled() { + Log.d(TAG, "setRemoteControllerEnabled"); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + mRemoteController = new RemoteController(TomahawkApp.getContext(), this); + Object service = TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE); + if (service instanceof AudioManager + && ((AudioManager) service).registerRemoteController(mRemoteController)) { + mRemoteController.setArtworkConfiguration(BITMAP_WIDTH, BITMAP_HEIGHT); + setSynchronizationMode(mRemoteController, + RemoteController.POSITION_SYNCHRONIZATION_CHECK); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Object service = + TomahawkApp.getContext().getSystemService(Context.MEDIA_SESSION_SERVICE); + if (service instanceof MediaSessionManager) { + MediaSessionManager manager = (MediaSessionManager) service; + ComponentName componentName = + new ComponentName(this, RemoteControllerService.class); + mSessionsChangedListener + = new MediaSessionManager.OnActiveSessionsChangedListener() { + @Override + public void onActiveSessionsChanged(List controllers) { + synchronized (this) { + mActiveSessions = controllers; + registerSessionCallbacks(); + } + } + }; + manager.addOnActiveSessionsChangedListener(mSessionsChangedListener, componentName); + synchronized (this) { + mActiveSessions = manager.getActiveSessions(componentName); + registerSessionCallbacks(); + } + } + } + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + setRemoteControllerDisabled(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void registerSessionCallbacks() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for (MediaController controller : mActiveSessions) { + if (mSessionCallback == null) { + mSessionCallback = new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + if (metadata != null) { + String trackName = + metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + String artistName = + metadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + String albumArtistName = + metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST); + String albumName = + metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); + MicroService.scrobbleTrack(trackName, artistName, albumName, + albumArtistName); + } + } + }; + } + controller.registerCallback(mSessionCallback); + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void unregisterSessionCallbacks() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mSessionCallback != null) { + for (MediaController controller : mActiveSessions) { + controller.unregisterCallback(mSessionCallback); + } + } + } + + /** + * Disables RemoteController. + */ + public void setRemoteControllerDisabled() { + Log.d(TAG, "setRemoteControllerDisabled"); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + Object service = TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE); + if (service instanceof AudioManager + && ((AudioManager) service).registerRemoteController(mRemoteController)) { + ((AudioManager) service).unregisterRemoteController(mRemoteController); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Object service = + TomahawkApp.getContext().getSystemService(Context.MEDIA_SESSION_SERVICE); + if (service instanceof MediaSessionManager) { + MediaSessionManager manager = (MediaSessionManager) service; + if (mSessionsChangedListener != null) { + manager.removeOnActiveSessionsChangedListener(mSessionsChangedListener); + } + synchronized (this) { + unregisterSessionCallbacks(); + mActiveSessions = new ArrayList<>(); + } + } + } + } + + @Override + public void onNotificationPosted(StatusBarNotification notification) { + } + + @Override + public void onNotificationRemoved(StatusBarNotification notification) { + } + + @Override + public void onClientChange(boolean arg0) { + } + + @Override + public void onClientMetadataUpdate(RemoteController.MetadataEditor arg0) { + Log.d(TAG, "onClientMetadataUpdate"); + String trackName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_TITLE, null); + String artistName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_ARTIST, null); + String albumArtistName = arg0 + .getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, null); + String albumName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM, null); + MicroService.scrobbleTrack(trackName, artistName, albumName, albumArtistName); + } + + @Override + public void onClientPlaybackStateUpdate(int arg0) { + } + + @Override + public void onClientPlaybackStateUpdate(int arg0, long arg1, long arg2, float arg3) { + } + + @Override + public void onClientTransportControlUpdate(int arg0) { + } + + /** + * This method lets us avoid a bug in RemoteController which results in an exception when + * calling RemoteController#setSynchronizationMode(int) (doesn't seem to work though) + */ + private void setSynchronizationMode(RemoteController controller, int sync) { + if ((sync != RemoteController.POSITION_SYNCHRONIZATION_NONE) && (sync + != RemoteController.POSITION_SYNCHRONIZATION_CHECK)) { + throw new IllegalArgumentException("Unknown synchronization mode " + sync); + } + + Class iRemoteControlDisplayClass; + + try { + iRemoteControlDisplayClass = Class.forName("android.media.IRemoteControlDisplay"); + } catch (ClassNotFoundException e1) { + throw new RuntimeException( + "Class IRemoteControlDisplay doesn't exist, can't access it with reflection"); + } + + Method remoteControlDisplayWantsPlaybackPositionSyncMethod; + try { + remoteControlDisplayWantsPlaybackPositionSyncMethod = AudioManager.class + .getDeclaredMethod("remoteControlDisplayWantsPlaybackPositionSync", + iRemoteControlDisplayClass, boolean.class); + remoteControlDisplayWantsPlaybackPositionSyncMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "Method remoteControlDisplayWantsPlaybackPositionSync() doesn't exist, can't access it with reflection"); + } + + Object rcDisplay; + Field rcDisplayField; + try { + rcDisplayField = RemoteController.class.getDeclaredField("mRcd"); + rcDisplayField.setAccessible(true); + rcDisplay = rcDisplayField.get(mRemoteController); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Field mRcd doesn't exist, can't access it with reflection"); + } catch (IllegalAccessException e) { + throw new RuntimeException("Field mRcd can't be accessed - access denied"); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Field mRcd can't be accessed - invalid argument"); + } + + AudioManager am = + (AudioManager) TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE); + try { + remoteControlDisplayWantsPlaybackPositionSyncMethod + .invoke(am, iRemoteControlDisplayClass.cast(rcDisplay), true); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - access denied"); + } catch (IllegalArgumentException e) { + throw new RuntimeException( + "Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - invalid arguments"); + } catch (InvocationTargetException e) { + throw new RuntimeException( + "Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - invalid invocation target"); + } + } + + /** + * Send a keyEvent with the given keyCode to the RemoteController + * + * @param keyCode the keyCode that should be send to the RemoteController + * @return return true if both clicks (up and down) were delivered successfully + */ + private boolean sendKeyEvent(int keyCode) { + //send "down" and "up" keyevents. + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + boolean first = mRemoteController.sendMediaKeyEvent(keyEvent); + keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); + boolean second = mRemoteController.sendMediaKeyEvent(keyEvent); + + return first && second; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/services/TomahawkMediaBrowserService.java b/app/src/main/java/org/tomahawk/tomahawk_android/services/TomahawkMediaBrowserService.java new file mode 100644 index 000000000..01daaf53f --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/services/TomahawkMediaBrowserService.java @@ -0,0 +1,71 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.services; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class TomahawkMediaBrowserService extends MediaBrowserServiceCompat { + + private static final String TAG = TomahawkMediaBrowserService.class.getSimpleName(); + + public static final String MEDIA_ID_ROOT = "__ROOT__"; + + public static final String MEDIA_ID_ALBUMS = "__ALBUMS__"; + + MediaSessionCompat mSession; + + @Override + public void onCreate() { + super.onCreate(); + + Log.d(TAG, "oncreate"); + // Start a new MediaSession + mSession = new MediaSessionCompat(this, "TomahawkMediaBrowserService"); + setSessionToken(mSession.getSessionToken()); + } + + @Override + public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, + Bundle rootHints) { + Log.d(TAG, "onGetRoot"); + return new BrowserRoot(MEDIA_ID_ROOT, null); + } + + @Override + public void onLoadChildren(@NonNull final String parentMediaId, + @NonNull final Result> result) { + Log.d(TAG, "OnLoadChildren.ROOT"); + List mediaItems = new ArrayList<>(); + MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() + .setMediaId("__ALBUMS__") + .setTitle("test") + .build(); + mediaItems.add(new MediaBrowserCompat.MediaItem(description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)); + result.sendResult(mediaItems); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigCheckbox.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigCheckbox.java new file mode 100644 index 000000000..67697b1c1 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigCheckbox.java @@ -0,0 +1,44 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.support.v7.widget.AppCompatCheckBox; +import android.util.AttributeSet; + +public class ConfigCheckbox extends AppCompatCheckBox implements ConfigFieldView { + + public String mConfigFieldId; + + public ConfigCheckbox(Context context) { + super(context); + } + + public ConfigCheckbox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public Object getValue() { + return isChecked(); + } + + public String getConfigFieldId() { + return mConfigFieldId; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigDropDown.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigDropDown.java new file mode 100644 index 000000000..d28fa0bd0 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigDropDown.java @@ -0,0 +1,44 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.support.v7.widget.AppCompatSpinner; +import android.util.AttributeSet; + +public class ConfigDropDown extends AppCompatSpinner implements ConfigFieldView { + + public String mConfigFieldId; + + public ConfigDropDown(Context context) { + super(context); + } + + public ConfigDropDown(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public Object getValue() { + return getSelectedItemPosition(); + } + + public String getConfigFieldId() { + return mConfigFieldId; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigEdittext.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigEdittext.java new file mode 100644 index 000000000..834c86f0d --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigEdittext.java @@ -0,0 +1,44 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.support.v7.widget.AppCompatEditText; +import android.util.AttributeSet; + +public class ConfigEdittext extends AppCompatEditText implements ConfigFieldView { + + public String mConfigFieldId; + + public ConfigEdittext(Context context) { + super(context); + } + + public ConfigEdittext(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public Object getValue() { + return getText().toString(); + } + + public String getConfigFieldId() { + return mConfigFieldId; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigFieldView.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigFieldView.java new file mode 100644 index 000000000..d0bfda783 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/ConfigFieldView.java @@ -0,0 +1,25 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +public interface ConfigFieldView { + + Object getValue(); + + String getConfigFieldId(); +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareHeightFrameLayout.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareHeightFrameLayout.java new file mode 100644 index 000000000..cee8d3879 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareHeightFrameLayout.java @@ -0,0 +1,48 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2012, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * A layout which will always show as a square {@link android.view.View}, because its height is also + * used as its width. + */ +public class SquareHeightFrameLayout extends FrameLayout { + + /** + * Constructs a new {@link SquareHeightFrameLayout} + */ + public SquareHeightFrameLayout(Context context) { + super(context); + } + + /** + * Constructs a new {@link SquareHeightFrameLayout} + */ + public SquareHeightFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(heightMeasureSpec, heightMeasureSpec); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthFrameLayout.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthFrameLayout.java new file mode 100644 index 000000000..15bf70ed4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthFrameLayout.java @@ -0,0 +1,48 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * A {@link android.widget.LinearLayout} which will always show as a scare {@link + * android.view.View}, because its width is also used as its height. + */ +public class SquareWidthFrameLayout extends FrameLayout { + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.ui.widgets.SquareWidthFrameLayout} + */ + public SquareWidthFrameLayout(Context context) { + super(context); + } + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.ui.widgets.SquareWidthFrameLayout} + */ + public SquareWidthFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthLinearLayout.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthLinearLayout.java new file mode 100644 index 000000000..5191e5191 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthLinearLayout.java @@ -0,0 +1,48 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.ui.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +/** + * A {@link android.widget.LinearLayout} which will always show as a scare {@link + * android.view.View}, because its width is also used as its height. + */ +public class SquareWidthLinearLayout extends LinearLayout { + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.ui.widgets.SquareWidthLinearLayout} + */ + public SquareWidthLinearLayout(Context context) { + super(context); + } + + /** + * Constructs a new {@link org.tomahawk.tomahawk_android.ui.widgets.SquareWidthLinearLayout} + */ + public SquareWidthLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/src/org/tomahawk/tomahawk_android/SquareWidthRelativeLayout.java b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthRelativeLayout.java similarity index 87% rename from src/org/tomahawk/tomahawk_android/SquareWidthRelativeLayout.java rename to app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthRelativeLayout.java index 186a7096a..352a40864 100644 --- a/src/org/tomahawk/tomahawk_android/SquareWidthRelativeLayout.java +++ b/app/src/main/java/org/tomahawk/tomahawk_android/ui/widgets/SquareWidthRelativeLayout.java @@ -15,21 +15,20 @@ * You should have received a copy of the GNU General Public License * along with Tomahawk. If not, see . */ -package org.tomahawk.tomahawk_android; +package org.tomahawk.tomahawk_android.ui.widgets; import android.content.Context; import android.util.AttributeSet; import android.widget.RelativeLayout; /** - * @author Enno Gottschalk - * + * A relative layout which will always show as a scare {@link android.view.View}, because its width + * is also used as its height. */ public class SquareWidthRelativeLayout extends RelativeLayout { /** * Constructs a new {@link SquareWidthRelativeLayout} - * */ public SquareWidthRelativeLayout(Context context) { super(context); @@ -37,16 +36,11 @@ public SquareWidthRelativeLayout(Context context) { /** * Constructs a new {@link SquareWidthRelativeLayout} - * */ public SquareWidthRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); } - /* - * (non-Javadoc) - * @see android.widget.RelativeLayout#onMeasure(int, int) - */ @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/AnimationUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/AnimationUtils.java new file mode 100644 index 000000000..c23251d28 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/AnimationUtils.java @@ -0,0 +1,119 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.tomahawk.tomahawk_android.R; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.view.View; + +public class AnimationUtils { + + public static final int DURATION_CONTEXTMENU = 120; + + public static final int DURATION_PLAYBACKTOPPANEL = 200; + + public static final int DURATION_ARROWROTATE = 200; + + public static final int DURATION_PLAYBACKSEEKMODE = 200; + + public static final int DURATION_PLAYBACKSEEKMODE_ABORT = 100; + + public static final int DURATION_PLAYBACKFRAGMENT_BG = 500; + + public static void fade(final View view, int duration, final boolean isFadeIn) { + fade(view, duration, isFadeIn, false); + } + + public static void fade(final View view, int duration, final boolean isFadeIn, + final boolean onlyInvisible) { + float from = isFadeIn ? 0f : 1f; + float to = isFadeIn ? 1f : 0f; + fade(view, from, to, duration, isFadeIn, new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + if (isFadeIn) { + view.setVisibility(View.VISIBLE); + animation.removeListener(this); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!isFadeIn) { + view.setVisibility(onlyInvisible ? View.INVISIBLE : View.GONE); + animation.removeListener(this); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } + + public static void fade(final View view, float from, float to, int duration, + final boolean isFadeIn, Animator.AnimatorListener listener) { + if (view != null) { + view.setVisibility(View.VISIBLE); + if (!(view.getTag(R.id.animation_state) instanceof Boolean) + || (Boolean) view.getTag(R.id.animation_state) != isFadeIn) { + view.setTag(R.id.animation_state, isFadeIn); + ValueAnimator animator = ObjectAnimator.ofFloat(view, "alpha", from, to); + if (view.getTag(R.id.animation_animator) instanceof ValueAnimator) { + ValueAnimator previousAnimator = + (ValueAnimator) view.getTag(R.id.animation_animator); + previousAnimator.cancel(); + } + view.setTag(R.id.animation_animator, animator); + animator.setDuration(duration); + if (listener != null) { + animator.addListener(listener); + } + animator.start(); + } + } + } + + public static void moveY(final View view, float from, float to, int duration, + final boolean isReversed) { + if (view != null) { + view.setVisibility(View.VISIBLE); + if (!(view.getTag(R.id.animation_state) instanceof Boolean) + || (Boolean) view.getTag(R.id.animation_state) != isReversed) { + view.setTag(R.id.animation_state, isReversed); + ValueAnimator animator = ObjectAnimator + .ofFloat(view, "y", isReversed ? to : from, isReversed ? from : to); + if (view.getTag(R.id.animation_animator) instanceof ValueAnimator) { + ValueAnimator previousAnimator = + (ValueAnimator) view.getTag(R.id.animation_animator); + previousAnimator.cancel(); + } + view.setTag(R.id.animation_animator, animator); + animator.setDuration(duration); + animator.start(); + } + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/BlurTransformation.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/BlurTransformation.java new file mode 100644 index 000000000..7c9ac0d81 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/BlurTransformation.java @@ -0,0 +1,98 @@ +package org.tomahawk.tomahawk_android.utils; + +/** + * Copyright (C) 2015 Wasabeef + * + * 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. + */ + +import com.squareup.picasso.Transformation; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.support.v8.renderscript.Allocation; +import android.support.v8.renderscript.Element; +import android.support.v8.renderscript.RSRuntimeException; +import android.support.v8.renderscript.RenderScript; +import android.support.v8.renderscript.ScriptIntrinsicBlur; +import android.util.Log; + +public class BlurTransformation implements Transformation { + + private static String TAG = BlurTransformation.class.getSimpleName(); + + private static int MAX_RADIUS = 25; + + private static int DEFAULT_DOWN_SAMPLING = 1; + + private Context mContext; + + private int mRadius; + + private int mSampling; + + public BlurTransformation(Context context) { + this(context, MAX_RADIUS, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, int radius) { + this(context, radius, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, int radius, int sampling) { + mContext = context.getApplicationContext(); + mRadius = radius; + mSampling = sampling; + } + + @Override + public Bitmap transform(Bitmap source) { + + try { + int scaledWidth = source.getWidth() / mSampling; + int scaledHeight = source.getHeight() / mSampling; + + Bitmap bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + canvas.scale(1 / (float) mSampling, 1 / (float) mSampling); + Paint paint = new Paint(); + paint.setFlags(Paint.FILTER_BITMAP_FLAG); + canvas.drawBitmap(source, 0, 0, paint); + + RenderScript rs = RenderScript.create(mContext); + Allocation input = Allocation.createFromBitmap(rs, bitmap, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + Allocation output = Allocation.createTyped(rs, input.getType()); + ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + + blur.setInput(input); + blur.setRadius(mRadius); + blur.forEach(output); + output.copyTo(bitmap); + + source.recycle(); + rs.destroy(); + + return bitmap; + } catch (RSRuntimeException e) { + Log.e(TAG, "transform - ", e); + return source; + } + } + + @Override + public String key() { + return "BlurTransformation(radius=" + mRadius + ", sampling=" + mSampling + ")"; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/ColorTintTransformation.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ColorTintTransformation.java new file mode 100644 index 000000000..aebe1a7b9 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ColorTintTransformation.java @@ -0,0 +1,69 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import com.squareup.picasso.Transformation; + +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; + +import static android.graphics.Bitmap.createBitmap; +import static android.graphics.Paint.ANTI_ALIAS_FLAG; + +public class ColorTintTransformation implements Transformation { + + private int mColorResId; + + public ColorTintTransformation(int colorResId) { + mColorResId = colorResId; + } + + @Override + public Bitmap transform(Bitmap source) { + Bitmap result = createBitmap(source.getWidth(), source.getHeight(), source.getConfig()); + + ColorFilter filter = getColorFilter(mColorResId); + + Paint paint = new Paint(ANTI_ALIAS_FLAG); + paint.setColorFilter(filter); + + Canvas canvas = new Canvas(result); + canvas.drawBitmap(source, 0, 0, paint); + + source.recycle(); + + return result; + } + + @Override + public String key() { + return "ColorTintTransformation"; + } + + public static ColorFilter getColorFilter(int colorResId) { + return new PorterDuffColorFilter( + TomahawkApp.getContext().getResources().getColor(colorResId), + PorterDuff.Mode.MULTIPLY); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/CropCircleTransformation.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/CropCircleTransformation.java new file mode 100644 index 000000000..715f088e7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/CropCircleTransformation.java @@ -0,0 +1,61 @@ +package org.tomahawk.tomahawk_android.utils; + +/** + * Copyright (C) 2015 Wasabeef + * + * 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. + */ + +import com.squareup.picasso.Transformation; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; + +public class CropCircleTransformation implements Transformation { + + @Override public Bitmap transform(Bitmap source) { + int size = Math.min(source.getWidth(), source.getHeight()); + + int width = (source.getWidth() - size) / 2; + int height = (source.getHeight() - size) / 2; + + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + BitmapShader shader = + new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); + if (width != 0 || height != 0) { + // source isn't square, move viewport to center + Matrix matrix = new Matrix(); + matrix.setTranslate(-width, -height); + shader.setLocalMatrix(matrix); + } + paint.setShader(shader); + paint.setAntiAlias(true); + + float r = size / 2f; + canvas.drawCircle(r, r, r, paint); + + source.recycle(); + + return bitmap; + } + + @Override public String key() { + return "CropCircleTransformation()"; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/DelayedHandler.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/DelayedHandler.java new file mode 100644 index 000000000..ddc45fcc8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/DelayedHandler.java @@ -0,0 +1,58 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import android.os.Message; + +public abstract class DelayedHandler extends WeakReferenceHandler { + + private int mDelay; + + private long mStartingTime = 0L; + + private int mDelayReduction = 0; + + public DelayedHandler(T referencedObject, int delay) { + super(referencedObject); + + mDelay = delay; + } + + public abstract void handleMessage(Message msg); + + public void reset() { + mDelayReduction = 0; + } + + public void start() { + if (mDelay - mDelayReduction > 0) { + mStartingTime = System.currentTimeMillis(); + removeCallbacksAndMessages(null); + Message message = obtainMessage(); + sendMessageDelayed(message, mDelay - mDelayReduction); + } + } + + public void stop() { + int delayReduction = (int) (System.currentTimeMillis() - mStartingTime); + if (delayReduction > 0) { + mDelayReduction += delayReduction; + } + removeCallbacksAndMessages(null); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/FakePreferenceGroup.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FakePreferenceGroup.java new file mode 100644 index 000000000..43d021a82 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FakePreferenceGroup.java @@ -0,0 +1,71 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2013, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import java.util.ArrayList; + +/** + * A group of several {@link FakePreference}s + */ +public class FakePreferenceGroup { + + public static final int TYPE_CHECKBOX = 1; + + public static final int TYPE_PLAIN = 2; + + public static final int TYPE_SPINNER = 3; + + private final ArrayList mFakePreferences = new ArrayList<>(); + + /** + * A {@link FakePreference} contains all information needed to provide the {@link + * org.tomahawk.tomahawk_android.adapters.FakePreferencesAdapter} with the necessary values to + * be displayed inside the Fragment's {@link android.widget.ListView} + */ + public static class FakePreference { + + // this FakePreference's type (Dialog, Checkbox, Plain or Spinner) + public int type; + + // the key to identify this FakePreference + public String id; + + // the key to store preferences with + public String storageKey; + + public String title; + + // short summary text to describe this FakePreference to the user + public String summary; + } + + /** + * Add a {@link FakePreference} to this {@link FakePreferenceGroup} + */ + public void addFakePreference(FakePreference fakePreference) { + mFakePreferences.add(fakePreference); + } + + /** + * @return an {@link ArrayList} of all {@link FakePreference}s + */ + public ArrayList getFakePreferences() { + return mFakePreferences; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentInfo.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentInfo.java new file mode 100644 index 000000000..4312a0dc7 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentInfo.java @@ -0,0 +1,14 @@ +package org.tomahawk.tomahawk_android.utils; + +import android.os.Bundle; + +public class FragmentInfo { + + public Class mClass; + + public String mTitle; + + public Bundle mBundle; + + public int mIconResId; +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentUtils.java new file mode 100644 index 000000000..2a99afeb8 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/FragmentUtils.java @@ -0,0 +1,248 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.infosystem.SocialAction; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.fragments.CollectionPagerFragment; +import org.tomahawk.tomahawk_android.fragments.ContentHeaderFragment; +import org.tomahawk.tomahawk_android.fragments.ContextMenuFragment; +import org.tomahawk.tomahawk_android.fragments.PlaybackFragment; +import org.tomahawk.tomahawk_android.fragments.SocialActionsFragment; +import org.tomahawk.tomahawk_android.fragments.TomahawkFragment; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.util.Log; + +/** + * This class wraps all functionality that handles the switching of {@link Fragment}s, whenever the + * user navigates to a new {@link Fragment}. + */ +public class FragmentUtils { + + private static final String TAG = FragmentUtils.class.getSimpleName(); + + public static final String FRAGMENT_TAG = "the_ultimate_tag"; + + public static final String ROOT_FRAGMENT_TAG = "root_fragment_tag"; + + public static final String PLAYBACK_FRAGMENT_TAG = "playback_fragment_tag"; + + /** + * Add a root {@link Fragment} as the first {@link Fragment} the user is seeing after opening + * the app. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param loggedInUser the currently logged-in user object. determines whether to show the feed + * fragment or collection fragment + */ + public static void addRootFragment(TomahawkMainActivity activity, User loggedInUser) { + if (activity.getSupportFragmentManager().findFragmentByTag(ROOT_FRAGMENT_TAG) == null) { + FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction(); + Fragment fragment; + if (!loggedInUser.isOffline()) { + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.USER, loggedInUser.getId()); + bundle.putInt(TomahawkFragment.SHOW_MODE, + SocialActionsFragment.SHOW_MODE_DASHBOARD); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_ACTIONBAR_FILLED); + fragment = Fragment.instantiate(activity, SocialActionsFragment.class.getName(), + bundle); + Log.d(TAG, "Added " + SocialActionsFragment.class.getSimpleName() + + " as root fragment."); + } else { + Bundle bundle = new Bundle(); + bundle.putString(TomahawkFragment.COLLECTION_ID, + TomahawkApp.PLUGINNAME_USERCOLLECTION); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_STATIC); + fragment = Fragment.instantiate(activity, CollectionPagerFragment.class.getName(), + bundle); + Log.d(TAG, "Added " + CollectionPagerFragment.class.getSimpleName() + + " as root fragment."); + } + ft.add(R.id.content_viewer_frame, fragment, ROOT_FRAGMENT_TAG); + ft.commitAllowingStateLoss(); + } + } + + /** + * Add a {@link PlaybackFragment} to the container inside the {@link + * com.sothree.slidinguppanel.SlidingUpPanelLayout} + * + * @param activity {@link TomahawkMainActivity} needed as a context object + */ + public static void addPlaybackFragment(TomahawkMainActivity activity) { + if (activity.getSupportFragmentManager().findFragmentByTag(PLAYBACK_FRAGMENT_TAG) == null) { + Bundle bundle = new Bundle(); + bundle.putInt(TomahawkFragment.CONTENT_HEADER_MODE, + ContentHeaderFragment.MODE_HEADER_PLAYBACK); + replace(activity, PlaybackFragment.class, bundle, R.id.playback_fragment_frame, + PLAYBACK_FRAGMENT_TAG); + } + } + + /** + * Replaces the current {@link Fragment} in the main fragment container. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param clss Class of the {@link Fragment} to instantiate + * @param bundle {@link Bundle} which contains arguments (can be null) + */ + public static void replace(TomahawkMainActivity activity, Class clss, Bundle bundle) { + replace(activity, clss, bundle, R.id.content_viewer_frame, FRAGMENT_TAG); + } + + /** + * Replaces the current {@link Fragment} in the container with the given id. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param clss Class of the {@link Fragment} to instantiate + * @param bundle {@link Bundle} which contains arguments (can be null) + * @param containerResId the resource id of the {@link android.view.ViewGroup} in which the + * Fragment will be replaced + */ + public static void replace(TomahawkMainActivity activity, Class clss, Bundle bundle, + int containerResId) { + replace(activity, clss, bundle, containerResId, FRAGMENT_TAG); + } + + /** + * Replaces the current {@link Fragment} in the container with the given id. Allows to specify + * the {@link String} with which to tag the replaced {@link Fragment}. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param clss Class of the {@link Fragment} to instantiate + * @param bundle {@link Bundle} which contains arguments (can be null) + * @param containerResId the resource id of the {@link android.view.ViewGroup} in which the + * Fragment will be replaced + * @param tag a {@link String} id to tag the replaced {@link Fragment} with + */ + public static void replace(TomahawkMainActivity activity, Class clss, Bundle bundle, + int containerResId, String tag) { + FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction(); + ft.replace(containerResId, Fragment.instantiate(activity, clss.getName(), bundle), tag); + if (containerResId == R.id.content_viewer_frame) { + ft.addToBackStack(clss.getName()); + } + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + ft.commitAllowingStateLoss(); + activity.collapsePanel(); + Log.d(TAG, "Current fragment is now " + clss.getSimpleName() + ", Bundle: " + bundle); + } + + /** + * Add the given {@link Fragment} to the container with the given id. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param clss Class of the {@link Fragment} to instantiate + * @param bundle {@link Bundle} which contains arguments (can be null) + * @param containerResId the resource id of the {@link android.view.ViewGroup} to which the + * Fragment will be added + */ + public static void add(TomahawkMainActivity activity, Class clss, Bundle bundle, + int containerResId) { + FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction(); + ft.add(containerResId, Fragment.instantiate(activity, clss.getName(), bundle), + FRAGMENT_TAG); + ft.addToBackStack(clss.getName()); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + ft.commitAllowingStateLoss(); + Log.d(TAG, "Added fragment " + clss.getSimpleName() + ", Bundle: " + bundle); + } + + /** + * Show the context menu for the given item in the given context. + * + * @param activity {@link TomahawkMainActivity} needed as a context object + * @param item The item for which to show the context menu + * @param collectionId the id of the corresponding {@link org.tomahawk.libtomahawk.collection.Collection} + * (this is being used to e.g. get an album's track list from a specific + * collection) + */ + public static boolean showContextMenu(TomahawkMainActivity activity, Object item, + String collectionId, boolean isFromPlaybackFragment, boolean hideRemoveButton) { + if (item == null + || (item instanceof SocialAction + && ((SocialAction) item).getTargetObject() instanceof User) + || item instanceof User) { + return false; + } + + Bundle args = new Bundle(); + if (collectionId != null) { + args.putString(TomahawkFragment.COLLECTION_ID, collectionId); + } + if (isFromPlaybackFragment) { + args.putBoolean(TomahawkFragment.FROM_PLAYBACKFRAGMENT, true); + args.putBoolean(TomahawkFragment.HIDE_REMOVE_BUTTON, true); + } else if (hideRemoveButton) { + args.putBoolean(TomahawkFragment.HIDE_REMOVE_BUTTON, true); + } + if (item instanceof Query) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((Query) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.QUERY); + } else if (item instanceof Album) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((Album) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.ALBUM); + } else if (item instanceof Artist) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((Artist) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.ARTIST); + } else if (item instanceof SocialAction) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((SocialAction) item).getId()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.SOCIALACTION); + } else if (item instanceof PlaylistEntry) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((PlaylistEntry) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.PLAYLISTENTRY); + } else if (item instanceof StationPlaylist) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((StationPlaylist) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.STATION); + } else if (item instanceof Playlist) { + args.putString(TomahawkFragment.TOMAHAWKLISTITEM, + ((Playlist) item).getCacheKey()); + args.putString(TomahawkFragment.TOMAHAWKLISTITEM_TYPE, + TomahawkFragment.PLAYLIST); + } + add(activity, ContextMenuFragment.class, args, R.id.context_menu_frame); + return true; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/IdGenerator.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/IdGenerator.java new file mode 100644 index 000000000..c62ad9d92 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/IdGenerator.java @@ -0,0 +1,35 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +public class IdGenerator { + + private static long mSessionIdCounter = 0; + + public static long getSessionUniqueId() { + return mSessionIdCounter++; + } + + public static String getSessionUniqueStringId() { + return String.valueOf(getSessionUniqueId()); + } + + public static String getLifetimeUniqueStringId() { + return String.valueOf(System.currentTimeMillis()) + getSessionUniqueStringId(); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaBrowserHelper.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaBrowserHelper.java new file mode 100644 index 000000000..594256f93 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaBrowserHelper.java @@ -0,0 +1,341 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionCursor; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.HatchetCollection; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class MediaBrowserHelper { + + private static final String TAG = MediaBrowserHelper.class.getSimpleName(); + + public static final String MEDIA_ID_ROOT = "__ROOT__"; + + public static final String MEDIA_ID_SHUFFLEDPLAY = "__SHUFFLEDPLAY__"; + + public static final String MEDIA_ID_SHUFFLEDPLAY_TRACKS = "__SHUFFLEDPLAY_TRACKS__"; + + public static final String MEDIA_ID_SHUFFLEDPLAY_FAVORITES = "__SHUFFLEDPLAY_FAVORITES__"; + + public static final String MEDIA_ID_COLLECTION = "__COLLECTION__"; + + public static final String MEDIA_ID_COLLECTION_ALBUMS = "__COLLECTION_ALBUMS__"; + + public static final String MEDIA_ID_COLLECTION_ARTISTS = "__COLLECTION_ARTISTS__"; + + public static final String MEDIA_ID_PLAYLISTS = "__PLAYLISTS__"; + + public static final String MEDIA_ID_STATIONS = "__STATIONS__"; + + private PackageValidator mPackageValidator; + + private Context mContext; + + public MediaBrowserHelper(Context context) { + mContext = context; + mPackageValidator = new PackageValidator(context); + } + + @Nullable + public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull String clientPackageName, + int clientUid, + @Nullable Bundle rootHints) { + Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + + "; clientUid=" + clientUid + " ; rootHints=" + rootHints); + // To ensure you are not allowing any arbitrary app to browse your app's contents, you + // need to check the origin: + if (!mPackageValidator.isCallerAllowed(mContext, clientPackageName, clientUid)) { + // If the request comes from an untrusted package, return null. No further calls will + // be made to other media browsing methods. + Log.e(TAG, "OnGetRoot: IGNORING request from untrusted package " + + clientPackageName); + return null; + } + return new MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_ROOT, null); + } + + public void onLoadChildren(@NonNull String parentId, + @NonNull final MediaBrowserServiceCompat.Result> result) { + Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentId); + + result.detach(); + + final List mediaItems = new ArrayList<>(); + + if (MEDIA_ID_ROOT.equals(parentId)) { + Log.d(TAG, "OnLoadChildren.ROOT"); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_SHUFFLEDPLAY) + .setTitle(mContext.getString(R.string.mediabrowser_shuffledplay)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_COLLECTION) + .setTitle(mContext.getString(R.string.drawer_title_collection)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_PLAYLISTS) + .setTitle(mContext.getString(R.string.drawer_title_playlists)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_STATIONS) + .setTitle(mContext.getString(R.string.drawer_title_stations)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + result.sendResult(mediaItems); + } else if (MEDIA_ID_SHUFFLEDPLAY.equals(parentId)) { + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_SHUFFLEDPLAY_FAVORITES) + .setTitle(mContext.getString(R.string.drawer_title_lovedtracks)) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_SHUFFLEDPLAY_TRACKS) + .setTitle(mContext.getString(R.string.tracks)) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + result.sendResult(mediaItems); + } else if (MEDIA_ID_COLLECTION.equals(parentId)) { + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_COLLECTION_ALBUMS) + .setTitle(mContext.getString(R.string.albums)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(MEDIA_ID_COLLECTION_ARTISTS) + .setTitle(mContext.getString(R.string.artists)) + .build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + )); + result.sendResult(mediaItems); + } else if (MEDIA_ID_COLLECTION_ALBUMS.equals(parentId)) { + CollectionManager.get().getUserCollection().getAlbums( + Collection.SORT_ALPHA).done( + new DoneCallback>() { + @Override + public void onDone(CollectionCursor albums) { + for (int i = 0; i < albums.size(); i++) { + Album album = albums.get(i); + String mediaId = buildMediaId(MEDIA_ID_COLLECTION_ALBUMS, + album.getCacheKey()); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle(album.getPrettyName()) + .setSubtitle(album.getArtist().getPrettyName()) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + } + albums.close(); + result.sendResult(mediaItems); + } + }); + } else if (MEDIA_ID_COLLECTION_ARTISTS.equals(parentId)) { + CollectionManager.get().getUserCollection().getArtists( + Collection.SORT_ALPHA).done( + new DoneCallback>() { + @Override + public void onDone(CollectionCursor artists) { + for (int i = 0; i < artists.size(); i++) { + Artist artist = artists.get(i); + String mediaId = buildMediaId(MEDIA_ID_COLLECTION_ARTISTS, + artist.getCacheKey()); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle(artist.getPrettyName()) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + } + artists.close(); + result.sendResult(mediaItems); + } + }); + } else if (MEDIA_ID_PLAYLISTS.equals(parentId)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + for (int i = 0; i < user.getPlaylists().size(); i++) { + Playlist playlist = user.getPlaylists().get(i); + String topArtistsString = ""; + String[] artists = playlist.getTopArtistNames(); + if (artists != null) { + for (int j = 0; j < artists.length && j < 5; j++) { + topArtistsString += artists[j]; + if (j != artists.length - 1) { + topArtistsString += ", "; + } + } + } + String mediaId = buildMediaId(MEDIA_ID_PLAYLISTS, playlist.getCacheKey()); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle(playlist.getName()) + .setSubtitle(topArtistsString) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + } + result.sendResult(mediaItems); + } + }); + } else if (MEDIA_ID_STATIONS.equals(parentId)) { + List stations = DatabaseHelper.get().getStations(); + for (StationPlaylist station : stations) { + String mediaId = buildMediaId(MEDIA_ID_STATIONS, station.getCacheKey()); + mediaItems.add(new MediaBrowserCompat.MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle(station.getName()) + .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + )); + } + result.sendResult(mediaItems); + } + } + + private String buildMediaId(String leaf, String cacheKey) { + return leaf + "♠" + cacheKey; + } + + /** + * Override to handle requests to play a specific mediaId that was provided by your app. + */ + public void onPlayFromMediaId(final MediaSessionCompat mediaSession, + final PlaybackManager playbackManager, final String mediaId, Bundle extras) { + String[] parts; + final MediaControllerCompat.TransportControls transportControls = + mediaSession.getController().getTransportControls(); + if (MEDIA_ID_SHUFFLEDPLAY_FAVORITES.equals(mediaId)) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + playbackManager.setPlaylist(user.getFavorites()); + playbackManager.setShuffleMode(PlaybackManager.SHUFFLED); + transportControls.play(); + } + }); + } else if (MEDIA_ID_SHUFFLEDPLAY_TRACKS.equals(mediaId)) { + CollectionManager.get().getUserCollection().getQueries(Collection.SORT_ALPHA).done( + new DoneCallback() { + @Override + public void onDone(Playlist collectionTracks) { + playbackManager.setPlaylist(collectionTracks); + playbackManager.setShuffleMode(PlaybackManager.SHUFFLED); + transportControls.play(); + } + }); + } else if ((parts = mediaId.split("♠", 2)).length > 1) { + String leaf = parts[0]; + String cacheKey = parts[1]; + if (MEDIA_ID_COLLECTION_ALBUMS.equals(leaf)) { + final Album album = Album.getByKey(cacheKey); + CollectionManager.get().getUserCollection().getAlbumTracks(album).done( + new DoneCallback() { + @Override + public void onDone(Playlist albumTracks) { + if (albumTracks != null) { + playbackManager.setPlaylist(albumTracks); + transportControls.play(); + } else { + HatchetCollection hatchetCollection + = CollectionManager.get().getHatchetCollection(); + hatchetCollection.getAlbumTracks(album).done( + new DoneCallback() { + @Override + public void onDone(Playlist albumTracks) { + if (albumTracks != null) { + playbackManager.setPlaylist(albumTracks); + mediaSession.getController() + .getTransportControls().play(); + } + } + }); + } + } + }); + } else if (MEDIA_ID_COLLECTION_ARTISTS.equals(leaf)) { + final Artist artist = Artist.getByKey(cacheKey); + CollectionManager.get().getUserCollection().getArtistTracks(artist).done( + new DoneCallback() { + @Override + public void onDone(Playlist artistTracks) { + if (artistTracks != null) { + playbackManager.setPlaylist(artistTracks); + transportControls.play(); + } else { + HatchetCollection hatchetCollection + = CollectionManager.get().getHatchetCollection(); + hatchetCollection.getArtistTopHits(artist).done( + new DoneCallback() { + @Override + public void onDone(Playlist artistTopHits) { + if (artistTopHits != null) { + playbackManager.setPlaylist(artistTopHits); + transportControls.play(); + } + } + }); + } + } + }); + } else if (MEDIA_ID_PLAYLISTS.equals(leaf) || MEDIA_ID_STATIONS.equals(leaf)) { + Playlist playlist = Playlist.getByKey(cacheKey); + if (!(playlist instanceof StationPlaylist) && !playlist.isFilled()) { + playlist = DatabaseHelper.get().getPlaylist(playlist.getId()); + } + playbackManager.setPlaylist(playlist); + transportControls.play(); + } + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaImageHelper.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaImageHelper.java new file mode 100644 index 000000000..f7689330c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaImageHelper.java @@ -0,0 +1,138 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; + +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.libtomahawk.utils.ImageUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.listeners.MediaImageLoadedListener; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.LruCache; + +import java.util.ArrayList; +import java.util.List; + +public class MediaImageHelper { + + private static final String TAG = MediaImageHelper.class.getSimpleName(); + + private static final int MAX_ALBUM_ART_CACHE_SIZE = 5 * 1024 * 1024; + + private static class Holder { + + private static final MediaImageHelper instance = new MediaImageHelper(); + + } + + private List mListeners = new ArrayList<>(); + + private Bitmap mCachedPlaceHolder; + + private final LruCache mMediaImageCache = + new LruCache(MAX_ALBUM_ART_CACHE_SIZE) { + @Override + protected int sizeOf(Image key, Bitmap value) { + return value.getByteCount(); + } + }; + + private MediaImageTarget mMediaImageTarget; + + private class MediaImageTarget implements Target { + + private Image mImageToLoad; + + public MediaImageTarget(Image imageToLoad) { + mImageToLoad = imageToLoad; + } + + @Override + public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { + new Runnable() { + @Override + public void run() { + Bitmap copy = bitmap.copy(bitmap.getConfig(), false); + if (mImageToLoad != null) { + mMediaImageCache.put(mImageToLoad, copy); + } + for (MediaImageLoadedListener listener : mListeners) { + listener.onMediaImageLoaded(); + } + Log.d(TAG, "Setting lockscreen bitmap"); + } + }.run(); + } + + @Override + public void onBitmapFailed(Drawable drawable) { + } + + @Override + public void onPrepareLoad(Drawable drawable) { + } + } + + private MediaImageHelper() { + Drawable drawable = + TomahawkApp.getContext().getResources().getDrawable(R.drawable.album_placeholder); + mCachedPlaceHolder = ImageUtils.drawableToBitmap(drawable); + } + + public static MediaImageHelper get() { + return Holder.instance; + } + + public void addListener(MediaImageLoadedListener listener) { + mListeners.add(listener); + } + + public void removeListener(MediaImageLoadedListener listener) { + mListeners.remove(listener); + } + + public void loadMediaImage(final Image image) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (mMediaImageTarget == null || mMediaImageTarget.mImageToLoad != image) { + mMediaImageTarget = new MediaImageTarget(image); + ImageUtils.loadImageIntoBitmap(TomahawkApp.getContext(), image, + mMediaImageTarget, Image.getLargeImageSize(), false); + } + } + }); + } + + public Bitmap getCachedPlaceHolder() { + return mCachedPlaceHolder; + } + + public LruCache getMediaImageCache() { + return mMediaImageCache; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaNotification.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaNotification.java new file mode 100644 index 000000000..8163ad162 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaNotification.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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. + * + * Modified by Enno Gottschalk in 2016 + */ + +package org.tomahawk.tomahawk_android.utils; + +import org.apache.lucene.util.ArrayUtil; +import org.tomahawk.libtomahawk.collection.Image; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.services.PlaybackService; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.RemoteException; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v7.app.NotificationCompat; +import android.util.Log; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Keeps track of a notification and updates it automatically for a given MediaSession. Maintaining + * a visible notification (usually) guarantees that the music service won't be killed during + * playback. + */ +public class MediaNotification { + + private static final String TAG = MediaNotification.class.getSimpleName(); + + private static final int NOTIFICATION_ID = 412; + + public static final String ACTION_FAVORITE = "org.tomahawk.tomahawk_android.favorite"; + + public static final String ACTION_UNFAVORITE = "org.tomahawk.tomahawk_android.unfavorite"; + + public static final String ACTION_PAUSE = "org.tomahawk.tomahawk_android.pause"; + + public static final String ACTION_PLAY = "org.tomahawk.tomahawk_android.play"; + + public static final String ACTION_PREV = "org.tomahawk.tomahawk_android.prev"; + + public static final String ACTION_NEXT = "org.tomahawk.tomahawk_android.next"; + + private final PlaybackService mService; + + private MediaSessionCompat.Token mSessionToken; + + private MediaControllerCompat mController; + + private MediaControllerCompat.TransportControls mTransportControls; + + private final SparseArray mIntents = new SparseArray<>(); + + private PlaybackStateCompat mPlaybackState; + + private MediaMetadataCompat mMetadata; + + private NotificationCompat.Builder mNotificationBuilder; + + private NotificationManagerCompat mNotificationManager; + + private NotificationCompat.Action mPlayPauseAction; + + private NotificationCompat.Action mFavoriteAction; + + private int mNotificationColor; + + private boolean mStarted = false; + + private final MediaControllerCompat.Callback mCallback = new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + mPlaybackState = state; + Log.d(TAG, "Received new playback state"); + if (mStarted) { + updateNotificationPlaybackState(); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + Log.d(TAG, "Updated notification to new playback state. mStarted: " + mStarted); + } else { + Log.d(TAG, "Couldn't update playback state because notification is stopped"); + } + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + mMetadata = metadata; + Log.d(TAG, "Received new metadata "); + if (mStarted) { + updateNotificationMetadata(); + } else { + Log.d(TAG, "Couldn't update playback state because notification is stopped"); + } + } + + @Override + public void onSessionDestroyed() { + super.onSessionDestroyed(); + Log.d(TAG, "Session was destroyed, resetting to the new session token"); + try { + updateSessionToken(); + } catch (RemoteException e) { + Log.e(TAG, "Could not connect to media controller: ", e); + } + } + }; + + private BroadcastReceiver mActionReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + Log.d(TAG, "Received intent with action " + action); + if (ACTION_FAVORITE.equals(action)) { + mTransportControls.setRating(RatingCompat.newHeartRating(true)); + } else if (ACTION_UNFAVORITE.equals(action)) { + mTransportControls.setRating(RatingCompat.newHeartRating(false)); + } else if (ACTION_PAUSE.equals(action)) { + mTransportControls.pause(); + } else if (ACTION_PLAY.equals(action)) { + mTransportControls.play(); + } else if (ACTION_NEXT.equals(action)) { + mTransportControls.skipToNext(); + } else if (ACTION_PREV.equals(action)) { + mTransportControls.skipToPrevious(); + } + } + }; + + public MediaNotification(PlaybackService service) throws RemoteException { + mService = service; + updateSessionToken(); + + mNotificationColor = mService.getResources().getColor(R.color.notification_bg); + + mNotificationManager = NotificationManagerCompat.from(mService); + stopNotification(); + + String pkg = mService.getPackageName(); + mIntents.put(R.drawable.ic_action_favorites_small, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_FAVORITE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_action_favorites_small_underlined, + PendingIntent.getBroadcast(mService, 100, new Intent(ACTION_UNFAVORITE) + .setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_av_pause, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_av_play_arrow, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_player_previous_light, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_player_next_light, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + } + + /** + * Posts the notification and starts tracking the session to keep it updated. The notification + * will automatically be removed if the session is destroyed before {@link #stopNotification} is + * called. + */ + public void startNotification() { + mService.getCallbackHandler().post(new Runnable() { + @Override + public void run() { + if (!mStarted) { + Log.d(TAG, "Starting notification"); + mController.registerCallback(mCallback, mService.getCallbackHandler()); + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_FAVORITE); + filter.addAction(ACTION_UNFAVORITE); + filter.addAction(ACTION_NEXT); + filter.addAction(ACTION_PAUSE); + filter.addAction(ACTION_PLAY); + filter.addAction(ACTION_PREV); + mService.registerReceiver(mActionReceiver, filter); + + mMetadata = mController.getMetadata(); + mPlaybackState = mController.getPlaybackState(); + + mStarted = true; + // The notification must be updated after setting started to true + updateNotificationMetadata(); + } + } + }); + } + + /** + * Removes the notification and stops tracking the session. If the session was destroyed this + * has no effect. + */ + public void stopNotification() { + mService.getCallbackHandler().post(new Runnable() { + @Override + public void run() { + mStarted = false; + if (mController != null) { + mController.unregisterCallback(mCallback); + } + try { + mService.unregisterReceiver(mActionReceiver); + } catch (IllegalArgumentException ex) { + // ignore if the receiver is not registered. + } + mService.stopForeground(true); + Log.d(TAG, "Stopped notification"); + } + }); + } + + /** + * Update the state based on a change on the session token. Called either when we are running + * for the first time or when the media session owner has destroyed the session (see {@link + * android.media.session.MediaController.Callback#onSessionDestroyed()}) + */ + private void updateSessionToken() throws RemoteException { + MediaSessionCompat.Token freshToken = mService.getSessionToken(); + if (mSessionToken == null && freshToken != null || + mSessionToken != null && !mSessionToken.equals(freshToken)) { + if (mController != null) { + mController.unregisterCallback(mCallback); + } + mSessionToken = freshToken; + if (mSessionToken != null) { + mController = new MediaControllerCompat(mService, mSessionToken); + mTransportControls = mController.getTransportControls(); + if (mStarted) { + mController.registerCallback(mCallback, mService.getCallbackHandler()); + } + } + } + } + + private void updateNotificationMetadata() { + Log.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); + if (mMetadata == null || mPlaybackState == null) { + return; + } + + mNotificationBuilder = new NotificationCompat.Builder(mService); + + List showInCompact = new ArrayList<>(); + + updateFavoriteAction(); + showInCompact.add(mNotificationBuilder.mActions.size()); + mNotificationBuilder.addAction(mFavoriteAction); + + // If skip to previous action is enabled + if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { + NotificationCompat.Action action = new NotificationCompat.Action.Builder( + R.drawable.ic_player_previous_light, + mService.getString(R.string.playback_previous), + mIntents.get(R.drawable.ic_player_previous_light)).build(); + mNotificationBuilder.addAction(action); + } + + updatePlayPauseAction(); + showInCompact.add(mNotificationBuilder.mActions.size()); + mNotificationBuilder.addAction(mPlayPauseAction); + + // If skip to next action is enabled + if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { + NotificationCompat.Action action = new NotificationCompat.Action.Builder( + R.drawable.ic_player_next_light, + mService.getString(R.string.playback_next), + mIntents.get(R.drawable.ic_player_next_light)).build(); + showInCompact.add(mNotificationBuilder.mActions.size()); + mNotificationBuilder.addAction(action); + } + + MediaDescriptionCompat description = mMetadata.getDescription(); + + String playbackManagerId = + mController.getExtras().getString(PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + PlaybackManager playbackManager = PlaybackManager.getByKey(playbackManagerId); + Image image = playbackManager.getCurrentQuery().getImage(); + Bitmap art = null; + if (image != null) { + art = MediaImageHelper.get().getMediaImageCache().get(image); + } + if (art == null) { + art = MediaImageHelper.get().getCachedPlaceHolder(); + } + + mNotificationBuilder + .setStyle(new NotificationCompat.MediaStyle() + .setShowActionsInCompactView(ArrayUtil.toIntArray(showInCompact)) + .setMediaSession(mSessionToken) + .setShowCancelButton(true) + .setCancelButtonIntent(createCancelIntent())) + .setColor(mNotificationColor) + .setSmallIcon(R.drawable.ic_notification) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(createContentIntent()) + .setContentTitle(description.getTitle()) + .setContentText(description.getSubtitle()) + .setTicker(description.getTitle() + " - " + description.getSubtitle()) + .setLargeIcon(art) + .setOngoing(false); + + updateNotificationPlaybackState(); + + mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); + Log.d(TAG, "updateNotificationMetadata. Notification shown"); + } + + private PendingIntent createContentIntent() { + Intent intent = new Intent(mService, TomahawkMainActivity.class); + intent.setAction(TomahawkMainActivity.SHOW_PLAYBACKFRAGMENT_ON_STARTUP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent createCancelIntent() { + Intent intent = new Intent(mService, PlaybackService.class); + intent.setAction(PlaybackService.ACTION_STOP_NOTIFICATION); + return PendingIntent.getService(mService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private void updateFavoriteAction() { + Log.d(TAG, "updateFavoriteAction"); + String favoriteLabel; + int favoriteIcon; + RatingCompat rating = mMetadata.getRating(MediaMetadataCompat.METADATA_KEY_USER_RATING); + if (rating != null && rating.hasHeart()) { + favoriteLabel = mService.getString(R.string.playback_unfavorite); + favoriteIcon = R.drawable.ic_action_favorites_small_underlined; + } else { + favoriteLabel = mService.getString(R.string.playback_favorite); + favoriteIcon = R.drawable.ic_action_favorites_small; + } + if (mFavoriteAction == null) { + mFavoriteAction = new NotificationCompat.Action.Builder(favoriteIcon, + favoriteLabel, mIntents.get(favoriteIcon)).build(); + } else { + mFavoriteAction.icon = favoriteIcon; + mFavoriteAction.title = favoriteLabel; + mFavoriteAction.actionIntent = mIntents.get(favoriteIcon); + } + } + + private void updatePlayPauseAction() { + Log.d(TAG, "updatePlayPauseAction"); + String playPauseLabel; + int playPauseIcon; + if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { + playPauseLabel = mService.getString(R.string.playback_pause); + playPauseIcon = R.drawable.ic_av_pause; + } else { + playPauseLabel = mService.getString(R.string.playback_play); + playPauseIcon = R.drawable.ic_av_play_arrow; + } + if (mPlayPauseAction == null) { + mPlayPauseAction = new NotificationCompat.Action.Builder(playPauseIcon, + playPauseLabel, mIntents.get(playPauseIcon)).build(); + } else { + mPlayPauseAction.icon = playPauseIcon; + mPlayPauseAction.title = playPauseLabel; + mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon); + } + } + + private void updateNotificationPlaybackState() { + Log.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); + if (mPlaybackState == null || !mStarted) { + Log.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); + mService.stopForeground(true); + return; + } + if (mNotificationBuilder == null) { + Log.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. " + + "Ignoring request to update state!"); + return; + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT && + mPlaybackState.getPosition() >= 0 && + mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) { + Log.d(TAG, "updateNotificationPlaybackState. updating playback position to " + + (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000 + + " seconds"); + mNotificationBuilder + .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) + .setShowWhen(true) + .setUsesChronometer(true); + } else { + Log.d(TAG, "updateNotificationPlaybackState. hiding playback position"); + mNotificationBuilder + .setWhen(0) + .setShowWhen(false) + .setUsesChronometer(false); + } + + updatePlayPauseAction(); + + // Make sure that the notification can be dismissed by the user when we are not playing: + mNotificationBuilder + .setOngoing(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING); + if (mPlaybackState.getState() != PlaybackStateCompat.STATE_PLAYING) { + mService.stopForeground(false); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaPlayIntentHandler.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaPlayIntentHandler.java new file mode 100644 index 000000000..77f839485 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaPlayIntentHandler.java @@ -0,0 +1,262 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.libtomahawk.infosystem.InfoSystem; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGenerator; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorManager; +import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorSearchResult; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.resolver.ResultScoring; + +import android.app.SearchManager; +import android.os.Bundle; +import android.provider.MediaStore; +import android.support.v4.media.session.MediaControllerCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import de.greenrobot.event.EventBus; + +public class MediaPlayIntentHandler { + + private Artist mResolvingArtist; + + private Album mResolvingAlbum; + + private String mWaitingGenre; + + private final Set mCorrespondingRequestIds = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Set mCorrespondingQueries = + Collections.newSetFromMap(new ConcurrentHashMap()); + + private PlaybackManager mPlaybackManager; + + private MediaControllerCompat.TransportControls mTransportControls; + + @SuppressWarnings("unused") + public void onEvent(ScriptPlaylistGeneratorManager.GeneratorAddedEvent event) { + playGenre(); + } + + @SuppressWarnings("unused") + public void onEvent(PipeLine.ResultsEvent event) { + if (mCorrespondingQueries.contains(event.mQuery)) { + Playlist playlist; + if (event.mQuery.isFullTextQuery()) { + playlist = event.mQuery.getResultPlaylist(); + } else { + List queries = new ArrayList<>(); + queries.add(event.mQuery); + playlist = Playlist.fromQueryList(event.mQuery.getCacheKey(), "", "", queries); + } + playPlaylist(playlist, false); + } + } + + @SuppressWarnings("unused") + public void onEvent(InfoSystem.ResultsEvent event) { + if (mCorrespondingRequestIds.contains(event.mInfoRequestData.getRequestId())) { + if (mResolvingAlbum != null) { + CollectionManager.get().getHatchetCollection().getAlbumTracks(mResolvingAlbum).done( + new DoneCallback() { + @Override + public void onDone(Playlist result) { + if (result != null && result.size() > 0) { + playPlaylist(result, false); + } + } + } + ); + } else if (mResolvingArtist != null) { + CollectionManager.get().getHatchetCollection().getArtistTopHits(mResolvingArtist) + .done(new DoneCallback() { + @Override + public void onDone(Playlist result) { + if (result != null && result.size() > 0) { + playPlaylist(result, false); + } + } + } + ); + } + } + } + + public MediaPlayIntentHandler(MediaControllerCompat.TransportControls transportControls, + PlaybackManager playbackManager) { + mTransportControls = transportControls; + mPlaybackManager = playbackManager; + EventBus.getDefault().register(this); + } + + public void mediaPlayFromSearch(Bundle extras) { + mWaitingGenre = null; + mCorrespondingQueries.clear(); + mCorrespondingRequestIds.clear(); + mResolvingAlbum = null; + mResolvingArtist = null; + + String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); + String query = extras.getString(SearchManager.QUERY); + + // Some of these extras may not be available depending on the search mode + String album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM); + String artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST); + final String genre = extras.getString("android.intent.extra.genre"); + String playlist = extras.getString("android.intent.extra.playlist"); + String title = extras.getString(MediaStore.EXTRA_MEDIA_TITLE); + // Determine the search mode and use the corresponding extras + if (mediaFocus == null) { + // 'Unstructured' search mode (backward compatible) + Query q = Query.get(query, false); + mCorrespondingQueries.clear(); + mCorrespondingQueries.add(PipeLine.get().resolve(q)); + } else if (mediaFocus.compareTo("vnd.android.cursor.item/*") == 0) { + if (query != null && query.isEmpty()) { + // 'Any' search mode + CollectionManager.get().getUserCollection().getQueries(Collection.SORT_ALPHA).done( + new DoneCallback() { + @Override + public void onDone(Playlist collectionTracks) { + playPlaylist(collectionTracks, true); + } + }); + } else { + // 'Unstructured' search mode + Query q = Query.get(query, false); + mCorrespondingQueries.add(PipeLine.get().resolve(q)); + } + } else if (mediaFocus.compareTo(MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE) == 0) { + // 'Genre' search mode + mWaitingGenre = genre; + playGenre(); + } else if (mediaFocus.compareTo(MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) == 0) { + // 'Artist' search mode + final Artist a = Artist.get(artist); + CollectionManager.get().getUserCollection().getArtistTracks(a).done( + new DoneCallback() { + @Override + public void onDone(Playlist result) { + if (result != null && result.size() > 0) { + // There are some local tracks for the requested artist available + playPlaylist(result, false); + } else { + // Try fetching top-hits for the requested artist + mResolvingArtist = a; + mCorrespondingRequestIds.add(InfoSystem.get().resolve(a, true)); + } + } + }); + } else if (mediaFocus.compareTo(MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) == 0) { + // 'Album' search mode + final Album a = Album.get(album, Artist.get(artist)); + CollectionManager.get().getUserCollection().getAlbumTracks(a).done( + new DoneCallback() { + @Override + public void onDone(Playlist result) { + if (result != null && result.size() > 0) { + // There are some local tracks for the requested album available + playPlaylist(result, false); + } else { + // Try fetching top-hits for the requested album + mResolvingAlbum = a; + mCorrespondingRequestIds.add(InfoSystem.get().resolve(a)); + } + } + }); + } else if (mediaFocus.compareTo("vnd.android.cursor.item/audio") == 0) { + // 'Song' search mode + Query q = Query.get(title, album, artist, false); + mCorrespondingQueries.add(PipeLine.get().resolve(q)); + } else if (mediaFocus.compareTo(MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE) == 0) { + // 'Playlist' search mode + Playlist bestMatch = null; + float bestScore = 0f; + float minScore = 0.7f; + for (Playlist pl : DatabaseHelper.get().getPlaylists()) { + float score = ResultScoring.calculateScore(pl.getName(), playlist); + if (score > minScore && score > bestScore) { + bestMatch = pl; + bestScore = score; + } + } + if (bestMatch != null) { + playPlaylist(bestMatch, false); + } + } + } + + private void playPlaylist(Playlist playlist, boolean shuffled) { + mPlaybackManager.setPlaylist(playlist); + if (shuffled) { + mPlaybackManager.setShuffleMode(PlaybackManager.SHUFFLED); + } + mTransportControls.play(); + EventBus.getDefault().unregister(this); + } + + private void playGenre() { + ScriptPlaylistGenerator generator = + ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator(); + if (generator != null && mWaitingGenre != null) { + final String genre = mWaitingGenre; + mWaitingGenre = null; + generator.search(genre).done(new DoneCallback() { + @Override + public void onDone(ScriptPlaylistGeneratorSearchResult result) { + if (result.mGenres.size() > 0) { + String bestMatch = null; + float bestScore = 0f; + float minScore = 0.7f; + for (String genreResult : result.mGenres) { + float score = ResultScoring.calculateScore(genreResult, + genre.toLowerCase()); + if (score > minScore && score > bestScore) { + bestMatch = genreResult; + bestScore = score; + } + } + if (bestMatch != null) { + List list = new ArrayList<>(); + list.add(bestMatch); + StationPlaylist pl = StationPlaylist.get(null, null, list); + playPlaylist(pl, false); + } + } + } + }); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaWrapper.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaWrapper.java new file mode 100644 index 000000000..b291ed012 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MediaWrapper.java @@ -0,0 +1,531 @@ +/***************************************************************************** + * MediaWrapper.java + ***************************************************************************** + * Copyright © 2011-2015 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +package org.tomahawk.tomahawk_android.utils; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import org.videolan.libvlc.Media; +import org.videolan.libvlc.Media.Meta; +import org.videolan.libvlc.Media.VideoTrack; +import org.videolan.libvlc.MediaPlayer; +import org.videolan.libvlc.util.Extensions; + +import java.util.Locale; + +public class MediaWrapper implements Parcelable { + public final static String TAG = "VLC/MediaWrapper"; + + public final static int TYPE_ALL = -1; + public final static int TYPE_VIDEO = 0; + public final static int TYPE_AUDIO = 1; + public final static int TYPE_GROUP = 2; + public final static int TYPE_DIR = 3; + public final static int TYPE_SUBTITLE = 4; + public final static int TYPE_PLAYLIST = 5; + + public final static int MEDIA_VIDEO = 0x01; + public final static int MEDIA_NO_HWACCEL = 0x02; + public final static int MEDIA_PAUSED = 0x4; + public final static int MEDIA_FORCE_AUDIO = 0x8; + + protected String mTitle; + protected String mDisplayTitle; + private String mArtist; + private String mGenre; + private String mCopyright; + private String mAlbum; + private int mTrackNumber; + private int mDiscNumber; + private String mAlbumArtist; + private String mDescription; + private String mRating; + private String mDate; + private String mSettings; + private String mNowPlaying; + private String mPublisher; + private String mEncodedBy; + private String mTrackID; + private String mArtworkURL; + + private final Uri mUri; + private String mFilename; + private long mTime = 0; + /* -1 is a valid track (Disabled) */ + private int mAudioTrack = -2; + private int mSpuTrack = -2; + private long mLength = 0; + private int mType; + private int mWidth = 0; + private int mHeight = 0; + private Bitmap mPicture; + private boolean mIsPictureParsed; + private int mFlags = 0; + private long mLastModified = 0l; + + /** + * Create a new MediaWrapper + * @param uri Should not be null. + */ + public MediaWrapper(Uri uri) { + if (uri == null) + throw new NullPointerException("uri was null"); + + mUri = uri; + init(null); + } + + /** + * Create a new MediaWrapper + * @param media should be parsed and not NULL + */ + public MediaWrapper(Media media) { + if (media == null) + throw new NullPointerException("media was null"); + + mUri = media.getUri(); + init(media); + } + + @Override + public boolean equals(Object obj) { + if (mUri == ((MediaWrapper)obj).getUri()) + return true; + return false; + } + + private void init(Media media) { + mType = TYPE_ALL; + + if (media != null) { + if (media.isParsed()) { + mLength = media.getDuration(); + + for (int i = 0; i < media.getTrackCount(); ++i) { + final Media.Track track = media.getTrack(i); + if (track == null) + continue; + if (track.type == Media.Track.Type.Video) { + final Media.VideoTrack videoTrack = (VideoTrack) track; + mType = TYPE_VIDEO; + mWidth = videoTrack.width; + mHeight = videoTrack.height; + } else if (mType == TYPE_ALL && track.type == Media.Track.Type.Audio){ + mType = TYPE_AUDIO; + } + } + } + updateMeta(media); + if (mType == TYPE_ALL && media.getType() == Media.Type.Directory) + mType = TYPE_DIR; + } + defineType(); + } + + public void defineType() { + if (mType != TYPE_ALL) + return; + + String fileExt = null; + final int index = mUri.toString().indexOf('?'); + String location; + if (index == -1) + location = mUri.toString(); + else + location = mUri.toString().substring(0, index); + + int dotIndex = location.lastIndexOf("."); + if (dotIndex != -1) { + fileExt = location.substring(dotIndex).toLowerCase(Locale.ENGLISH); + } else { + // Try to get the extension from the title, as fallback. + dotIndex = mTitle != null ? mTitle.lastIndexOf(".") : -1; + + if (dotIndex != -1) { + fileExt = mTitle.substring(dotIndex).toLowerCase(Locale.ENGLISH); + } + } + + if (!TextUtils.isEmpty(fileExt)) { + if (Extensions.VIDEO.contains(fileExt)) { + mType = TYPE_VIDEO; + } else if (Extensions.AUDIO.contains(fileExt)) { + mType = TYPE_AUDIO; + } else if (Extensions.SUBTITLES.contains(fileExt)) { + mType = TYPE_SUBTITLE; + } else if (Extensions.PLAYLIST.contains(fileExt)) { + mType = TYPE_PLAYLIST; + } + } + } + + private void init(long time, long length, int type, + Bitmap picture, String title, String artist, String genre, String album, String albumArtist, + int width, int height, String artworkURL, int audio, int spu, int trackNumber, int discNumber, long lastModified) { + mFilename = null; + mTime = time; + mAudioTrack = audio; + mSpuTrack = spu; + mLength = length; + mType = type; + mPicture = picture; + mWidth = width; + mHeight = height; + + mTitle = title; + mArtist = artist; + mGenre = genre; + mAlbum = album; + mAlbumArtist = albumArtist; + mArtworkURL = artworkURL; + mTrackNumber = trackNumber; + mDiscNumber = discNumber; + mLastModified = lastModified; + } + + public MediaWrapper(Uri uri, long time, long length, int type, + Bitmap picture, String title, String artist, String genre, String album, String albumArtist, + int width, int height, String artworkURL, int audio, int spu, int trackNumber, int discNumber, long lastModified) { + mUri = uri; + init(time, length, type, picture, title, artist, genre, album, albumArtist, + width, height, artworkURL, audio, spu, trackNumber, discNumber, lastModified); + } + + public String getLocation() { + return mUri.toString(); + } + + public Uri getUri() { + return mUri; + } + + private static String getMetaId(Media media, int id, boolean trim) { + String meta = media.getMeta(id); + return meta != null ? trim ? meta.trim() : meta : null; + } + + public void updateMeta(Media media) { + mTitle = getMetaId(media, Meta.Title, true); + mArtist = getMetaId(media, Meta.Artist, true); + mAlbum = getMetaId(media, Meta.Album, true); + mGenre = getMetaId(media, Meta.Genre, true); + mAlbumArtist = getMetaId(media, Meta.AlbumArtist, true); + mArtworkURL = getMetaId(media, Meta.ArtworkURL, false); + mNowPlaying = getMetaId(media, Meta.NowPlaying, false); + final String trackNumber = getMetaId(media, Meta.TrackNumber, false); + if (!TextUtils.isEmpty(trackNumber)) { + try { + mTrackNumber = Integer.parseInt(trackNumber); + } catch (NumberFormatException ignored) {} + } + final String discNumber = getMetaId(media, Meta.DiscNumber, false); + if (!TextUtils.isEmpty(discNumber)) { + try { + mDiscNumber = Integer.parseInt(discNumber); + } catch (NumberFormatException ignored) {} + } + } + + public void updateMeta(MediaPlayer mediaPlayer) { + final Media media = mediaPlayer.getMedia(); + if (media == null) + return; + updateMeta(media); + media.release(); + } + + public String getFileName() { + if (mFilename == null) { + mFilename = mUri.getLastPathSegment(); + } + return mFilename; + } + + public long getTime() { + return mTime; + } + + public void setTime(long time) { + mTime = time; + } + + public int getAudioTrack() { + return mAudioTrack; + } + + public void setAudioTrack(int track) { + mAudioTrack = track; + } + + public int getSpuTrack() { + return mSpuTrack; + } + + public void setSpuTrack(int track) { + mSpuTrack = track; + } + + public long getLength() { + return mLength; + } + + public int getType() { + return mType; + } + + public void setType(int type){ + mType = type; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + /** + * Returns the raw picture object. Likely to be NULL in VLC for Android + * due to lazy-loading. + * + * Use {@link BitmapUtil#getPictureFromCache(MediaWrapper)} instead. + * + * @return The raw picture or NULL + */ + public Bitmap getPicture() { + return mPicture; + } + + /** + * Sets the raw picture object. + * + * In VLC for Android, use {@link MediaDatabase#setPicture(MediaWrapper, Bitmap)} instead. + * + * @param p + */ + public void setPicture(Bitmap p) { + mPicture = p; + } + + public boolean isPictureParsed() { + return mIsPictureParsed; + } + + public void setPictureParsed(boolean isParsed) { + mIsPictureParsed = isParsed; + } + + public void setDisplayTitle(String title){ + mDisplayTitle = title; + } + + public void setArtist(String artist){ + mArtist = artist; + } + + public String getTitle() { + if (!TextUtils.isEmpty(mDisplayTitle)) + return mDisplayTitle; + if (!TextUtils.isEmpty(mTitle)) + return mTitle; + String fileName = getFileName(); + if (fileName == null) + return ""; + int end = fileName.lastIndexOf("."); + if (end <= 0) + return fileName; + return fileName.substring(0, end); + } + + public String getReferenceArtist() { + return mAlbumArtist == null ? mArtist : mAlbumArtist; + } + + public String getArtist() { + return mArtist; + } + + public Boolean isArtistUnknown() { + return mArtist == null; + } + + public String getGenre() { + if (mGenre == null) + return null; + else if (mGenre.length() > 1)/* Make genres case insensitive via normalisation */ + return Character.toUpperCase(mGenre.charAt(0)) + mGenre.substring(1).toLowerCase(Locale.getDefault()); + else + return mGenre; + } + + public String getCopyright() { + return mCopyright; + } + + public String getAlbum() { + return mAlbum; + } + + public String getAlbumArtist() { + return mAlbumArtist; + } + + public Boolean isAlbumUnknown() { + return mAlbum == null; + } + + public int getTrackNumber() { + return mTrackNumber; + } + + public int getDiscNumber() { + return mDiscNumber; + } + + public void setDescription(String description){ + mDescription = description; + } + + public String getDescription() { + return mDescription; + } + + public String getRating() { + return mRating; + } + + public String getDate() { + return mDate; + } + + public String getSettings() { + return mSettings; + } + + public String getNowPlaying() { + return mNowPlaying; + } + + public String getPublisher() { + return mPublisher; + } + + public String getEncodedBy() { + return mEncodedBy; + } + + public String getTrackID() { + return mTrackID; + } + + public String getArtworkURL() { + return mArtworkURL; + } + + public void setArtworkURL(String url) { + mArtworkURL = url; + } + + public long getLastModified() { + return mLastModified; + } + + public void setLastModified(long mLastModified) { + this.mLastModified = mLastModified; + } + + public void addFlags(int flags) { + mFlags |= flags; + } + public void setFlags(int flags) { + mFlags = flags; + } + public int getFlags() { + return mFlags; + } + public boolean hasFlag(int flag) { + return (mFlags & flag) != 0; + } + public void removeFlags(int flags) { + mFlags &= ~flags; + } + + @Override + public int describeContents() { + return 0; + } + + public MediaWrapper(Parcel in) { + mUri = in.readParcelable(Uri.class.getClassLoader()); + init(in.readLong(), + in.readLong(), + in.readInt(), + (Bitmap) in.readParcelable(Bitmap.class.getClassLoader()), + in.readString(), + in.readString(), + in.readString(), + in.readString(), + in.readString(), + in.readInt(), + in.readInt(), + in.readString(), + in.readInt(), + in.readInt(), + in.readInt(), + in.readInt(), + in.readLong()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mUri, flags); + dest.writeLong(getTime()); + dest.writeLong(getLength()); + dest.writeInt(getType()); + dest.writeParcelable(getPicture(), flags); + dest.writeString(getTitle()); + dest.writeString(getArtist()); + dest.writeString(getGenre()); + dest.writeString(getAlbum()); + dest.writeString(getAlbumArtist()); + dest.writeInt(getWidth()); + dest.writeInt(getHeight()); + dest.writeString(getArtworkURL()); + dest.writeInt(getAudioTrack()); + dest.writeInt(getSpuTrack()); + dest.writeInt(getTrackNumber()); + dest.writeInt(getDiscNumber()); + dest.writeLong(getLastModified()); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public MediaWrapper createFromParcel(Parcel in) { + return new MediaWrapper(in); + } + public MediaWrapper[] newArray(int size) { + return new MediaWrapper[size]; + } + }; +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/MenuDrawer.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MenuDrawer.java new file mode 100644 index 000000000..55b824990 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/MenuDrawer.java @@ -0,0 +1,175 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.jdeferred.DoneCallback; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.libtomahawk.authentication.HatchetAuthenticatorUtils; +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.collection.CollectionManager; +import org.tomahawk.libtomahawk.collection.ScriptResolverCollection; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.adapters.TomahawkMenuAdapter; +import org.tomahawk.tomahawk_android.listeners.MenuDrawerListener; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Handler; +import android.os.Looper; +import android.support.v4.widget.DrawerLayout; +import android.util.AttributeSet; +import android.util.Log; + +import java.util.ArrayList; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class MenuDrawer extends DrawerLayout { + + private final static String TAG = MenuDrawer.class.getSimpleName(); + + public static final String HUB_ID_USERPAGE = "userpage"; + + public static final String HUB_ID_FEED = "feed"; + + public static final String HUB_ID_CHARTS = "charts"; + + public static final String HUB_ID_COLLECTION = "collection"; + + public static final String HUB_ID_LOVEDTRACKS = "lovedtracks"; + + public static final String HUB_ID_PLAYLISTS = "playlists"; + + public static final String HUB_ID_STATIONS = "stations"; + + public static final String HUB_ID_SETTINGS = "settings"; + + public MenuDrawer(Context context) { + super(context); + } + + public MenuDrawer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public MenuDrawer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void updateDrawer(final TomahawkMainActivity activity) { + final StickyListHeadersListView drawerList = + (StickyListHeadersListView) findViewById(R.id.left_drawer); + if (drawerList == null) { + Log.e(TAG, "updateDrawer - Couldn't update drawer because drawerList is null"); + return; + } + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User user) { + HatchetAuthenticatorUtils authenticatorUtils + = (HatchetAuthenticatorUtils) AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET); + // Set up the TomahawkMenuAdapter. Give it its set of menu item texts and icons to display + final ArrayList holders = new ArrayList<>(); + TomahawkMenuAdapter.ResourceHolder holder + = new TomahawkMenuAdapter.ResourceHolder(); + Resources resources = activity.getResources(); + if (authenticatorUtils.isLoggedIn()) { + holder.id = HUB_ID_USERPAGE; + holder.title = user.getName(); + holder.image = user.getImage(); + holder.user = user; + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_FEED; + holder.title = resources.getString(R.string.drawer_title_feed); + holder.iconResId = R.drawable.ic_action_dashboard; + holders.add(holder); + } + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_CHARTS; + holder.title = resources.getString(R.string.drawer_title_charts); + holder.iconResId = R.drawable.ic_action_charts; + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_COLLECTION; + holder.title = resources.getString(R.string.drawer_title_collection); + holder.iconResId = R.drawable.ic_action_collection; + holder.isLoading = !CollectionManager.get().getUserCollection().isInitialized(); + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_LOVEDTRACKS; + holder.title = resources.getString(R.string.drawer_title_lovedtracks); + holder.iconResId = R.drawable.ic_action_favorites; + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_PLAYLISTS; + holder.title = resources.getString(R.string.drawer_title_playlists); + holder.iconResId = R.drawable.ic_action_playlist; + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_STATIONS; + holder.title = resources.getString(R.string.drawer_title_stations); + holder.iconResId = R.drawable.ic_action_station; + holders.add(holder); + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.id = HUB_ID_SETTINGS; + holder.title = resources.getString(R.string.drawer_title_settings); + holder.iconResId = R.drawable.ic_action_settings; + holders.add(holder); + for (Collection collection : CollectionManager.get().getCollections()) { + if (collection instanceof ScriptResolverCollection) { + ScriptResolverCollection resolverCollection + = (ScriptResolverCollection) collection; + holder = new TomahawkMenuAdapter.ResourceHolder(); + holder.collection = resolverCollection; + holder.isLoading = !resolverCollection.isInitialized(); + holders.add(holder); + } + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (drawerList.getAdapter() == null) { + drawerList.setAdapter(new TomahawkMenuAdapter(holders)); + } else { + ((TomahawkMenuAdapter) drawerList.getAdapter()) + .setResourceHolders(holders); + } + } + }); + + drawerList.setOnItemClickListener( + new MenuDrawerListener(activity, drawerList, MenuDrawer.this)); + } + }); + } + + public void closeDrawer() { + StickyListHeadersListView drawerList = + (StickyListHeadersListView) findViewById(R.id.left_drawer); + if (drawerList == null) { + Log.e(TAG, "closeDrawer - Couldn't close drawer because drawerList is null"); + return; + } + closeDrawer(drawerList); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/PackageValidator.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PackageValidator.java new file mode 100644 index 000000000..98a4a61da --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PackageValidator.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2014 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 org.tomahawk.tomahawk_android.utils; + +import org.tomahawk.tomahawk_android.R; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.Process; +import android.util.Base64; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Validates that the calling package is authorized to browse a {@link + * android.service.media.MediaBrowserService}. + * + * The list of allowed signing certificates and their corresponding package names is defined in + * res/xml/allowed_media_browser_callers.xml. + * + * If you add a new valid caller to allowed_media_browser_callers.xml and you don't know its + * signature, this class will print to logcat (INFO level) a message with the proper base64 version + * of the caller certificate that has not been validated. You can copy from logcat and paste into + * allowed_media_browser_callers.xml. Spaces and newlines are ignored. + */ +public class PackageValidator { + + private static final String TAG = PackageValidator.class.getSimpleName(); + + /** + * Map allowed callers' certificate keys to the expected caller information. + */ + private final Map> mValidCertificates; + + public PackageValidator(Context ctx) { + mValidCertificates = readValidCertificates(ctx.getResources().getXml( + R.xml.allowed_media_browser_callers)); + } + + private Map> readValidCertificates(XmlResourceParser parser) { + HashMap> validCertificates = new HashMap<>(); + try { + int eventType = parser.next(); + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG + && parser.getName().equals("signing_certificate")) { + + String name = parser.getAttributeValue(null, "name"); + String packageName = parser.getAttributeValue(null, "package"); + boolean isRelease = parser.getAttributeBooleanValue(null, "release", false); + String certificate = parser.nextText().replaceAll("\\s|\\n", ""); + + CallerInfo info = new CallerInfo(name, packageName, isRelease); + + ArrayList infos = validCertificates.get(certificate); + if (infos == null) { + infos = new ArrayList<>(); + validCertificates.put(certificate, infos); + } + Log.v(TAG, "Adding allowed caller: " + info.name + + " package=" + info.packageName + " release=" + info.release + + " certificate=" + certificate); + infos.add(info); + } + eventType = parser.next(); + } + } catch (XmlPullParserException | IOException e) { + Log.e(TAG, "Could not read allowed callers from XML: ", e); + } + return validCertificates; + } + + /** + * @return false if the caller is not authorized to get data from this MediaBrowserService + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isCallerAllowed(Context context, String callingPackage, int callingUid) { + // Always allow calls from the framework, self app or development environment. + if (Process.SYSTEM_UID == callingUid || Process.myUid() == callingUid) { + return true; + } + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo; + try { + packageInfo = packageManager.getPackageInfo( + callingPackage, PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Package manager can't find package: " + callingPackage + " :", e); + return false; + } + if (packageInfo.signatures.length != 1) { + Log.w(TAG, "Caller has more than one signature certificate!"); + return false; + } + String signature = Base64.encodeToString( + packageInfo.signatures[0].toByteArray(), Base64.NO_WRAP); + + // Test for known signatures: + ArrayList validCallers = mValidCertificates.get(signature); + if (validCallers == null) { + Log.v(TAG, "Signature for caller " + callingPackage + " is not valid: \n" + + signature); + if (mValidCertificates.isEmpty()) { + Log.w(TAG, "The list of valid certificates is empty. Either your file " + + "res/xml/allowed_media_browser_callers.xml is empty or there was an error " + + + "while reading it. Check previous log messages."); + } + return false; + } + + // Check if the package name is valid for the certificate: + StringBuffer expectedPackages = new StringBuffer(); + for (CallerInfo info : validCallers) { + if (callingPackage.equals(info.packageName)) { + Log.v(TAG, "Valid caller: " + info.name + " package=" + info.packageName + + " release=" + info.release); + return true; + } + expectedPackages.append(info.packageName).append(' '); + } + + Log.i(TAG, "Caller has a valid certificate, but its package doesn't match any " + + "expected package for the given certificate. Caller's package is " + callingPackage + + + ". Expected packages as defined in res/xml/allowed_media_browser_callers.xml are (" + + + expectedPackages + "). This caller's certificate is: \n" + signature); + + return false; + } + + private final static class CallerInfo { + + final String name; + + final String packageName; + + final boolean release; + + public CallerInfo(String name, String packageName, boolean release) { + this.name = name; + this.packageName = packageName; + this.release = release; + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/PlaybackManager.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PlaybackManager.java new file mode 100644 index 000000000..f488f5f8c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PlaybackManager.java @@ -0,0 +1,423 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.tomahawk.libtomahawk.collection.Cacheable; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.collection.PlaylistEntry; +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.collection.Track; +import org.tomahawk.libtomahawk.resolver.Query; + +import android.util.Log; + +import java.util.List; + +public class PlaybackManager extends Cacheable { + + public static final String TAG = PlaybackManager.class.getSimpleName(); + + public static final int NOT_REPEATING = 0; + + public static final int REPEAT_ALL = 1; + + public static final int REPEAT_ONE = 2; + + public static final int NOT_SHUFFLED = 0; + + public static final int SHUFFLED = 1; + + private int mRepeatMode = NOT_REPEATING; + + private int mShuffleMode = NOT_SHUFFLED; + + private Playlist mPlaylist = + Playlist.fromEmptyList(IdGenerator.getLifetimeUniqueStringId(), ""); + + private Playlist mQueue = + Playlist.fromEmptyList(IdGenerator.getLifetimeUniqueStringId(), ""); + + private int mQueueStartPos = 0; + + private PlaylistEntry mCurrentEntry; + + private int mCurrentIndex = -1; + + private String mId; + + private Callback mCallback; + + public interface Callback { + + void onPlaylistChanged(); + + void onCurrentEntryChanged(); + + void onShuffleModeChanged(); + + void onRepeatModeChanged(); + } + + private PlaybackManager(String id) { + super(PlaybackManager.class, id); + + mId = id; + } + + public static PlaybackManager get(String id) { + Cacheable cacheable = get(PlaybackManager.class, id); + return cacheable != null ? (PlaybackManager) cacheable : new PlaybackManager(id); + } + + public static PlaybackManager getByKey(String id) { + return (PlaybackManager) get(PlaybackManager.class, id); + } + + public String getId() { + return mId; + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public Playlist getPlaylist() { + return mPlaylist; + } + + public void setPlaylist(Playlist playlist) { + setPlaylist(playlist, null); + } + + public void setPlaylist(StationPlaylist playlist) { + PlaylistEntry currentEntry = null; + if (playlist.size() > 0) { + currentEntry = playlist.getEntryAtPos(playlist.size() - 1); + } + setPlaylist(playlist, currentEntry); + } + + public void setPlaylist(Playlist playlist, PlaylistEntry currentEntry) { + if (mCallback == null || playlist == null) { + Log.e(TAG, "setPlaylist failed: " + playlist); + return; + } + mRepeatMode = NOT_REPEATING; + mShuffleMode = NOT_SHUFFLED; + if (playlist instanceof StationPlaylist) { + mPlaylist = playlist; + } else { + mPlaylist = playlist.copy( + Playlist.get("playback_playlist" + IdGenerator.getSessionUniqueStringId())); + } + if (currentEntry == null) { + currentEntry = mPlaylist.getEntryAtPos(0); + } + setCurrentEntry(currentEntry, false); + mCallback.onPlaylistChanged(); + } + + /** + * @param position int containing the position in the current playback list + * @return the {@link PlaylistEntry} which has been found at the given position + */ + public PlaylistEntry getPlaybackListEntry(int position) { + if (position < mQueueStartPos) { + // The requested entry is positioned before the queue + return mPlaylist.getEntryAtPos(position, mShuffleMode == SHUFFLED); + } else if (position < mQueueStartPos + mQueue.size()) { + // Getting the entry from the queue + return mQueue.getEntryAtPos(position - mQueueStartPos); + } else { + // The requested entry is positioned after the queue + return mPlaylist.getEntryAtPos(position - mQueue.size(), mShuffleMode == SHUFFLED); + } + } + + /** + * @param entry The {@link PlaylistEntry} to get the index for + * @return an int containing the index of the given {@link PlaylistEntry} inside the current + * playback list + */ + public int getPlaybackListIndex(PlaylistEntry entry) { + int index = mQueue.getIndexOfEntry(entry); + if (index >= 0) { + // Found entry in queue + return index + mQueueStartPos; + } else { + index = mPlaylist.getIndexOfEntry(entry, mShuffleMode == SHUFFLED); + if (index < 0) { + if (entry != null) { + Log.e(TAG, "getPlaybackListIndex - Couldn't find given entry in mQueue or" + + " mPlaylist: " + entry.getQuery().getName()); + } + return -1; + } + if (index < mQueueStartPos) { + // Found entry and its positioned before the queue + return index; + } else { + // Found entry and its positioned after the queue + return index + mQueue.size(); + } + } + } + + public boolean isPartOfQueue(int position) { + return position >= mQueueStartPos && position < mQueueStartPos + mQueue.size(); + } + + public int getNumeration(int position) { + int numeration; + if (position < mQueueStartPos) { + // Positioned before the queue + numeration = position; + } else if (position < mQueueStartPos + mQueue.size()) { + // From the queue + numeration = position - mQueueStartPos; + } else { + // Positioned after the queue + numeration = position - mQueue.size(); + } + return numeration - mCurrentIndex; + } + + public int getPlaybackListSize() { + return mQueue.size() + mPlaylist.size(); + } + + public void setCurrentEntry(PlaylistEntry currentEntry) { + setCurrentEntry(currentEntry, true); + } + + private void setCurrentEntry(PlaylistEntry currentEntry, boolean callback) { + if (mCallback == null) { + Log.e(TAG, "setCurrentEntry failed: " + currentEntry); + return; + } + PlaylistEntry lastEntry = mCurrentEntry; + mCurrentEntry = currentEntry; + // Delete the last entry from the queue + boolean playlistChanged = mQueue.deleteEntry(lastEntry); + if (currentEntry == null) { + mCurrentIndex = 0; + if (mPlaylist.size() > 0) { + mQueueStartPos = mCurrentIndex + 1; + } else { + mQueueStartPos = 0; + } + playlistChanged = true; + } else if (mPlaylist.containsEntry(currentEntry)) { + // We have a PlaylistEntry that is not part of the Queue + mCurrentIndex = mPlaylist.getIndexOfEntry(currentEntry, mShuffleMode == SHUFFLED); + mQueueStartPos = mCurrentIndex + 1; + playlistChanged = true; + } else { + // We have a PlaylistEntry that is part of the Queue + mCurrentIndex = mQueue.getIndexOfEntry(currentEntry) + mQueueStartPos; + } + if (callback) { + if (playlistChanged) { + mCallback.onPlaylistChanged(); + } else { + mCallback.onCurrentEntryChanged(); + } + } + } + + public PlaylistEntry getCurrentEntry() { + return mCurrentEntry; + } + + public int getCurrentIndex() { + return mCurrentIndex; + } + + public Query getCurrentQuery() { + if (mCurrentEntry != null) { + return mCurrentEntry.getQuery(); + } + return null; + } + + public Track getCurrentTrack() { + if (mCurrentEntry != null) { + return mCurrentEntry.getQuery().getPreferredTrack(); + } + return null; + } + + public void addToPlaylist(Query query) { + Log.d(TAG, "addToPlaylist: " + query); + if (mCallback == null) { + Log.e(TAG, "addToPlaylist failed: " + query); + return; + } + mPlaylist.addQuery(mPlaylist.size(), query); + if (getCurrentEntry() == null) { + setCurrentEntry(getPlaybackListEntry(0), false); + } + mCallback.onPlaylistChanged(); + } + + public void addToQueue(Query query) { + Log.d(TAG, "addToQueue: " + query); + if (mCallback == null) { + Log.e(TAG, "addToQueue failed: " + query); + return; + } + mQueue.addQuery(mQueue.size(), query); + if (getCurrentEntry() == null) { + setCurrentEntry(mQueue.getEntryAtPos(0), false); + } + mCallback.onPlaylistChanged(); + } + + public void addToQueue(List queries) { + Log.d(TAG, "addToQueue: queries.size()= " + queries.size()); + if (mCallback == null) { + Log.e(TAG, "addToQueue failed: queries.size()= " + queries.size()); + return; + } + int counter = 0; + for (Query query : queries) { + mQueue.addQuery(counter++, query); + } + if (getCurrentEntry() == null) { + setCurrentEntry(getPlaybackListEntry(0), false); + } + mCallback.onPlaylistChanged(); + } + + public void deleteFromQueue(PlaylistEntry entry) { + Log.d(TAG, "deleteFromQueue: " + entry); + if (mCallback == null) { + Log.e(TAG, "deleteFromQueue failed: " + entry); + return; + } + if (mQueue.deleteEntry(entry)) { + mCallback.onPlaylistChanged(); + } + } + + public PlaylistEntry getNextEntry() { + return getNextEntry(mCurrentEntry); + } + + public PlaylistEntry getNextEntry(PlaylistEntry entry) { + if (entry == null) { + return null; + } + if (mRepeatMode == REPEAT_ONE) { + return entry; + } + int index = getPlaybackListIndex(entry); + PlaylistEntry nextEntry = getPlaybackListEntry(index + 1); + if (nextEntry == null && mRepeatMode == REPEAT_ALL) { + nextEntry = getPlaybackListEntry(0); + } + return nextEntry; + } + + public boolean hasNextEntry() { + return hasNextEntry(mCurrentEntry); + } + + public boolean hasNextEntry(PlaylistEntry entry) { + return mRepeatMode == REPEAT_ONE || getNextEntry(entry) != null; + } + + public PlaylistEntry getPreviousEntry() { + return getPreviousEntry(mCurrentEntry); + } + + public PlaylistEntry getPreviousEntry(PlaylistEntry entry) { + if (entry == null) { + return null; + } + if (mRepeatMode == REPEAT_ONE) { + return entry; + } + int index = getPlaybackListIndex(entry); + PlaylistEntry previousEntry = getPlaybackListEntry(index - 1); + if (previousEntry == null && mRepeatMode == REPEAT_ALL) { + previousEntry = getPlaybackListEntry(getPlaybackListSize() - 1); + } + return previousEntry; + } + + public boolean hasPreviousEntry() { + return hasPreviousEntry(mCurrentEntry); + } + + public boolean hasPreviousEntry(PlaylistEntry entry) { + return mRepeatMode == REPEAT_ONE || getPreviousEntry(entry) != null; + } + + public int getRepeatMode() { + return mRepeatMode; + } + + public void setRepeatMode(int repeatingMode) { + Log.d(TAG, "repeat from " + mRepeatMode + " to " + repeatingMode); + if (mCallback == null) { + Log.e(TAG, "setRepeatMode failed: " + repeatingMode); + return; + } + if (mRepeatMode != repeatingMode) { + mRepeatMode = repeatingMode; + mCallback.onRepeatModeChanged(); + } + } + + public int getShuffleMode() { + return mShuffleMode; + } + + /** + * Set whether or not to enable shuffle mode on the current playlist. + */ + public void setShuffleMode(int shuffleMode) { + Log.d(TAG, "shuffle from " + mShuffleMode + " to " + shuffleMode); + if (mCallback == null) { + Log.e(TAG, "setShuffleMode failed: " + shuffleMode); + return; + } + if (mShuffleMode != shuffleMode) { + mShuffleMode = shuffleMode; + if (mShuffleMode == SHUFFLED) { + int currentIndex = -1; + if (!mQueue.containsEntry(mCurrentEntry)) { + // We have a PlaylistEntry that is not part of the Queue + currentIndex = mPlaylist.getIndexOfEntry(mCurrentEntry); + } + mPlaylist.buildShuffledIndex(currentIndex); + } + if (!mQueue.containsEntry(mCurrentEntry)) { + // mCurrentEntry not part of queue, refresh mCurrentIndex + setCurrentEntry(mCurrentEntry); + } else { + // mCurrentEntry is part of queue, put the queue at the very top + mQueueStartPos = 0; + } + mCallback.onPlaylistChanged(); + mCallback.onShuffleModeChanged(); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/PluginUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PluginUtils.java new file mode 100644 index 000000000..000b502ab --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PluginUtils.java @@ -0,0 +1,87 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import com.google.android.gms.common.GooglePlayServicesUtil; + +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.mediaplayers.DeezerMediaPlayer; +import org.tomahawk.tomahawk_android.mediaplayers.SpotifyMediaPlayer; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +public class PluginUtils { + + public static boolean isPluginInstalled(String pluginName) { + String pluginPackageName = ""; + switch (pluginName) { + case TomahawkApp.PLUGINNAME_SPOTIFY: + pluginPackageName = SpotifyMediaPlayer.PACKAGE_NAME; + break; + case TomahawkApp.PLUGINNAME_DEEZER: + pluginPackageName = DeezerMediaPlayer.PACKAGE_NAME; + break; + } + try { + TomahawkApp.getContext().getPackageManager() + .getPackageInfo(pluginPackageName, PackageManager.GET_SERVICES); + return true; + } catch (PackageManager.NameNotFoundException ignored) { + } + return false; + } + + public static boolean isPluginUpToDate(String pluginName) { + String pluginPackageName = ""; + int pluginMinVersionCode = 0; + switch (pluginName) { + case TomahawkApp.PLUGINNAME_SPOTIFY: + pluginPackageName = SpotifyMediaPlayer.PACKAGE_NAME; + pluginMinVersionCode = SpotifyMediaPlayer.MIN_VERSION; + break; + case TomahawkApp.PLUGINNAME_DEEZER: + pluginPackageName = DeezerMediaPlayer.PACKAGE_NAME; + pluginMinVersionCode = DeezerMediaPlayer.MIN_VERSION; + break; + } + try { + PackageInfo info = TomahawkApp.getContext().getPackageManager() + .getPackageInfo(pluginPackageName, PackageManager.GET_SERVICES); + // Remove the first digit that identifies the architecture type + String versionCodeString = String.valueOf(info.versionCode); + versionCodeString = versionCodeString.substring(1, versionCodeString.length()); + int versionCode = Integer.valueOf(versionCodeString); + if (versionCode >= pluginMinVersionCode) { + return true; + } + } catch (PackageManager.NameNotFoundException ignored) { + } + return false; + } + + public static boolean isPlayStoreInstalled() { + try { + TomahawkApp.getContext().getPackageManager() + .getPackageInfo(GooglePlayServicesUtil.GOOGLE_PLAY_STORE_PACKAGE, 0); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/PreferenceUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PreferenceUtils.java new file mode 100644 index 000000000..4b3e2ad35 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/PreferenceUtils.java @@ -0,0 +1,224 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.tomahawk.libtomahawk.authentication.AuthenticatorManager; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity; +import org.tomahawk.tomahawk_android.dialogs.AskAccessConfigDialog; + +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; +import android.support.v4.app.FragmentActivity; +import android.util.Log; + +import java.util.Set; + +public class PreferenceUtils { + + public static final String TAG = PreferenceUtils.class.getSimpleName(); + + /** + * USER PREFERENCES + */ + public static final String SCROBBLE_EVERYTHING + = "org.tomahawk.tomahawk_android.scrobbleeverything"; + + public static final String PLUG_IN_TO_PLAY + = "org.tomahawk.tomahawk_android.plugintoplay"; + + public static final String ASKED_FOR_ACCESS + = "org.tomahawk.tomahawk_android.asked_for_access"; + + public static final String PREF_BITRATE + = "org.tomahawk.tomahawk_android.prefbitrate"; + + /** + * See {@link org.tomahawk.tomahawk_android.R.array#fake_preferences_items_bitrate} + */ + public static final int PREF_BITRATE_LOW = 0; + + public static final int PREF_BITRATE_MEDIUM = 1; + + public static final int PREF_BITRATE_HIGH = 2; + + /** + * COACHMARK PREFERENCES + */ + public static final String COACHMARK_SEEK_DISABLED = "coachmark_seek_disabled"; + + public static final String COACHMARK_SEEK_TIMESTAMP = "coachmark_seek_timestamp"; + + public static final String COACHMARK_PLAYBACKFRAGMENT_NAVIGATION_DISABLED + = "coachmark_playbackfragment_navigation_disabled"; + + public static final String COACHMARK_WELCOMEFRAGMENT_DISABLED + = "coachmark_welcomefragment_disabled"; + + public static final String COACHMARK_SWIPELAYOUT_ENQUEUE_DISABLED + = "coachmark_swipelayout_enqueue_disabled"; + + + /** + * CHARTS PREFERENCES + */ + public static final String CHARTS_COUNTRY_CODE + = "org.tomahawk.tomahawk_android.charts_country_code"; + + public static final String LAST_DISPLAYED_PROVIDER_ID = + "org.tomahawk.tomahawk_android.last_displayed_provider_id"; + + + /** + * EQUALIZER PREFERENCES + */ + public final static String EQUALIZER_VALUES = "equalizer_values"; + + public final static String EQUALIZER_ENABLED = "equalizer_enabled"; + + public final static String EQUALIZER_PRESET = "equalizer_preset"; + + /** + * USERPAGE PREFERENCES + */ + public static final String USERPAGER_SELECTOR_POSITION + = "org.tomahawk.tomahawk_android.userpager_selector_position"; + + + private static final SharedPreferences mPreferences = + PreferenceManager.getDefaultSharedPreferences(TomahawkApp.getContext()); + + public static SharedPreferences.Editor edit() { + return mPreferences.edit(); + } + + private static boolean getBooleanDefault(String prefKey) { + if (prefKey.equals(SCROBBLE_EVERYTHING)) { + return true; + } + return false; + } + + private static int getIntDefault(String prefKey) { + if (prefKey.equals(PREF_BITRATE)) { + return PREF_BITRATE_MEDIUM; + } else if (prefKey.equals(EQUALIZER_PRESET)) { + return 0; + } else if (prefKey.equals(USERPAGER_SELECTOR_POSITION)) { + return 0; + } + return -1; + } + + private static String getStringDefault(String prefKey) { + return null; + } + + private static int getLongDefault(String prefKey) { + return -1; + } + + private static Set getStringSetDefault(String prefKey) { + return null; + } + + public static boolean getBoolean(String key) { + return mPreferences.getBoolean(key, getBooleanDefault(key)); + } + + public static String getString(String key) { + return mPreferences.getString(key, getStringDefault(key)); + } + + public static int getInt(String key) { + return getInt(key, getIntDefault(key)); + } + + public static int getInt(String key, int defaultValue) { + return mPreferences.getInt(key, defaultValue); + } + + public static long getLong(String key) { + return getLong(key, getLongDefault(key)); + } + + public static long getLong(String key, long defaultValue) { + return mPreferences.getLong(key, defaultValue); + } + + public static Set getStringSet(String key) { + return mPreferences.getStringSet(key, getStringSetDefault(key)); + } + + public static float[] getFloatArray(String key) { + float[] array = null; + String s = mPreferences.getString(key, null); + if (s != null) { + try { + JSONArray json = new JSONArray(s); + array = new float[json.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = (float) json.getDouble(i); + } + } catch (JSONException e) { + Log.e(TAG, "getFloatArray: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return array; + } + + public static void putFloatArray(SharedPreferences.Editor editor, String key, float[] array) { + try { + JSONArray json = new JSONArray(); + for (float f : array) { + json.put(f); + } + editor.putString(key, json.toString()); + } catch (JSONException e) { + Log.e(TAG, "putFloatArray: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + + /** + * Starts the AskAccessActivity in order to ask the user for permission to the notification + * listener, if the user hasn't been asked before and is logged into hatchet + */ + public static void attemptAskAccess(TomahawkMainActivity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (!getBoolean(ASKED_FOR_ACCESS)) { + askAccess(activity); + } + } + } + + /** + * Starts the AskAccessActivity in order to ask the user for permission to the notification + * listener, if the user is logged into Hatchet and we don't already have access + */ + public static void askAccess(FragmentActivity activity) { + if (AuthenticatorManager.get() + .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET).isLoggedIn()) { + mPreferences.edit().putBoolean(ASKED_FOR_ACCESS, true).apply(); + new AskAccessConfigDialog().show(activity.getSupportFragmentManager(), null); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/ProgressBarUpdater.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ProgressBarUpdater.java new file mode 100644 index 000000000..71af4dd37 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ProgressBarUpdater.java @@ -0,0 +1,92 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2016, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import android.os.Handler; +import android.support.v4.media.session.PlaybackStateCompat; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class ProgressBarUpdater { + + private static final long PROGRESS_UPDATE_INTERNAL = 500; + + private static final long PROGRESS_UPDATE_INITIAL_INTERVAL = 100; + + private final ScheduledExecutorService mExecutorService = + Executors.newSingleThreadScheduledExecutor(); + + private ScheduledFuture mScheduleFuture; + + private final Handler mHandler = new Handler(); + + private PlaybackStateCompat mPlaybackState; + + private long mCurrentDuration; + + private UpdateProgressRunnable mUpdateProgressRunnable; + + public interface UpdateProgressRunnable { + + void updateProgress(PlaybackStateCompat playbackState, long duration); + } + + public ProgressBarUpdater(UpdateProgressRunnable updateProgressRunnable) { + mUpdateProgressRunnable = updateProgressRunnable; + } + + public void setPlaybackState(PlaybackStateCompat playbackState) { + mPlaybackState = playbackState; + } + + public void setCurrentDuration(long currentDuration) { + mCurrentDuration = currentDuration; + } + + public void scheduleSeekbarUpdate() { + stopSeekbarUpdate(); + if (!mExecutorService.isShutdown()) { + mScheduleFuture = mExecutorService.scheduleAtFixedRate( + new Runnable() { + @Override + public void run() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mPlaybackState != null) { + mUpdateProgressRunnable.updateProgress(mPlaybackState, + mCurrentDuration); + } + } + }); + } + }, PROGRESS_UPDATE_INITIAL_INTERVAL, + PROGRESS_UPDATE_INTERNAL, TimeUnit.MILLISECONDS); + } + } + + public void stopSeekbarUpdate() { + if (mScheduleFuture != null) { + mScheduleFuture.cancel(false); + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/SearchViewStyle.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/SearchViewStyle.java new file mode 100644 index 000000000..26c1522d5 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/SearchViewStyle.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 Jared Rummler + * Ported to support lib by Enno Gottschalk + * + * 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 org.tomahawk.tomahawk_android.utils; + +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.SearchView; +import android.view.View; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; + +import java.lang.reflect.Field; + +/** + * Helper class to style a {@link SearchView}.

+ * + * Example usage:

+ * + *
+ * 
+ * SearchViewStyle.on(searchView)
+ *    .setCursorColor(Color.WHITE)
+ *    .setTextColor(Color.WHITE)
+ *    .setHintTextColor(Color.WHITE)
+ *    .setSearchHintDrawable(R.drawable.ic_search_api_material)
+ *    .setSearchButtonImageResource(R.drawable.ic_search_api_material)
+ *    .setCloseBtnImageResource(R.drawable.ic_clear_material)
+ *    .setVoiceBtnImageResource(R.drawable.ic_voice_search_api_material)
+ *    .setGoBtnImageResource(R.drawable.ic_go_search_api_material)
+ *    .setCommitIcon(R.drawable.ic_commit_search_api_material)
+ *    .setSubmitAreaDrawableId(R.drawable.abc_textfield_search_activated_mtrl_alpha)
+ *    .setSearchPlateDrawableId(R.drawable.abc_textfield_search_activated_mtrl_alpha)
+ *    .setSearchPlateTint(Color.WHITE)
+ *    .setSubmitAreaTint(Color.WHITE);
+ * 
+ * + * + * + * @author Jared Rummler + * @since Oct 24, 2014 + */ +public class SearchViewStyle { + + // =========================================================== + // STATIC METHODS + // =========================================================== + public static SearchViewStyle on(final SearchView searchView) { + return new SearchViewStyle(searchView); + } + + // =========================================================== + // FIELDS + // =========================================================== + private final SearchView mSearchView; + + // =========================================================== + // CONSTRUCTORS + // =========================================================== + private SearchViewStyle(final SearchView searchView) { + mSearchView = searchView; + } + + // =========================================================== + // METHODS + // =========================================================== + + @SuppressWarnings("unchecked") + public T getView(final int id) { + if (id == 0) { + return null; + } + View view = mSearchView.findViewById(id); + return (T) view; + } + + public SearchViewStyle setSearchPlateDrawableId(final int id) { + final View view = getView(android.support.v7.appcompat.R.id.search_plate); + if (view != null) { + view.setBackgroundResource(id); + } + return this; + } + + public SearchViewStyle setCursorColor(final int color) { + final AutoCompleteTextView editText = getView( + android.support.v7.appcompat.R.id.search_src_text); + if (editText != null) { + try { + final Field fCursorDrawableRes = TextView.class + .getDeclaredField("mCursorDrawableRes"); + fCursorDrawableRes.setAccessible(true); + final int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); + final Field fEditor = TextView.class.getDeclaredField("mEditor"); + fEditor.setAccessible(true); + final Object editor = fEditor.get(editText); + final Class clazz = editor.getClass(); + final Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); + fCursorDrawable.setAccessible(true); + final Drawable[] drawables = new Drawable[2]; + drawables[0] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); + drawables[1] = editText.getContext().getResources().getDrawable(mCursorDrawableRes); + drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); + fCursorDrawable.set(editor, drawables); + } catch (final Throwable ignored) { + } + } + return this; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/ShareUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ShareUtils.java new file mode 100644 index 000000000..afdf0c178 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ShareUtils.java @@ -0,0 +1,197 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.jdeferred.DoneCallback; +import org.jdeferred.Promise; +import org.tomahawk.libtomahawk.collection.Album; +import org.tomahawk.libtomahawk.collection.Artist; +import org.tomahawk.libtomahawk.collection.Playlist; +import org.tomahawk.libtomahawk.infosystem.User; +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.libtomahawk.utils.ADeferredObject; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; + +import android.app.Activity; +import android.content.Intent; +import android.util.Log; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +public class ShareUtils { + + private final static String TAG = ShareUtils.class.getSimpleName(); + + private static final String sHatchetBaseUrl = "https://hatchet.is/"; + + public static final String DEFAULT_SHARE_PREFIX = "#musthear"; + + public static void sendShareIntent(Activity activity, Album item) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, ShareUtils.generateShareMsg(item)); + activity.startActivity(shareIntent); + } + + public static String generateLink(Album album) { + if (album != null) { + String urlStr = sHatchetBaseUrl + "music/" + album.getArtist().getName() + "/" + + album.getName(); + try { + return escapeUrlString(urlStr); + } catch (MalformedURLException | URISyntaxException e) { + Log.e(TAG, "generateLink: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return null; + } + + public static String generateShareMsg(Album album) { + if (album != null) { + return DEFAULT_SHARE_PREFIX + + " " + TomahawkApp.getContext().getString(R.string.album_by_artist, + "\"" + album.getName() + "\"", album.getArtist().getName()) + + " - " + generateLink(album); + } + return null; + } + + private static String escapeUrlString(String urlString) + throws MalformedURLException, URISyntaxException { + URL url = new URL(urlString); + URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), + url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + return uri.toURL().toString(); + } + + public static void sendShareIntent(Activity activity, Artist item) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, ShareUtils.generateShareMsg(item)); + activity.startActivity(shareIntent); + } + + public static String generateLink(Artist artist) { + if (artist != null) { + String urlStr = sHatchetBaseUrl + "music/" + artist.getName(); + try { + return escapeUrlString(urlStr); + } catch (MalformedURLException | URISyntaxException e) { + Log.e(TAG, "generateLink: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return null; + } + + public static String generateShareMsg(Artist artist) { + if (artist != null) { + return DEFAULT_SHARE_PREFIX + " " + artist.getName() + " - " + generateLink(artist); + } + return null; + } + + + public static void sendShareIntent(Activity activity, Query item) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, ShareUtils.generateShareMsg(item)); + activity.startActivity(shareIntent); + } + + public static String generateLink(Query query) { + if (query != null) { + String urlStr = sHatchetBaseUrl + "music/" + query.getArtist().getName() + "/_/" + + query.getName(); + try { + return escapeUrlString(urlStr); + } catch (MalformedURLException | URISyntaxException e) { + Log.e(TAG, "generateLink: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return null; + } + + public static String generateShareMsg(Query query) { + if (query != null) { + return DEFAULT_SHARE_PREFIX + + " " + TomahawkApp.getContext().getString(R.string.album_by_artist, + "\"" + query.getName() + "\"", query.getArtist().getName()) + + " - " + generateLink(query); + } + return null; + } + + + public static void sendShareIntent(final Activity activity, Playlist item) { + generateShareMsg(item).done(new DoneCallback() { + @Override + public void onDone(String result) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, result); + activity.startActivity(shareIntent); + } + }); + } + + public static String generateLink(Playlist playlist, User user) { + if (playlist != null) { + String urlStr = sHatchetBaseUrl + "people/" + user.getName() + "/playlists/" + + playlist.getHatchetId(); + try { + return escapeUrlString(urlStr); + } catch (MalformedURLException | URISyntaxException e) { + Log.e(TAG, "generateLink: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return null; + } + + public static Promise generateShareMsg(final Playlist playlist) { + final ADeferredObject deferred = new ADeferredObject<>(); + if (playlist != null && playlist.getUserId() != null) { + final User user = User.getUserById(playlist.getUserId()); + if (user != null && user.getName() != null) { + User.getSelf().done(new DoneCallback() { + @Override + public void onDone(User result) { + String s; + if (user == result) { + s = TomahawkApp.getContext().getString(R.string.my_playlist); + } else { + s = TomahawkApp.getContext().getString( + R.string.users_playlist_suffix, user.getName()); + } + String msg = DEFAULT_SHARE_PREFIX + " " + s + ": \"" + playlist.getName() + + "\"" + " - " + generateLink(playlist, user); + deferred.resolve(msg); + } + }); + } else { + deferred.resolve(null); + } + } else { + deferred.resolve(null); + } + return deferred; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/ThreadManager.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ThreadManager.java new file mode 100644 index 000000000..cf7bd4072 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/ThreadManager.java @@ -0,0 +1,113 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.tomahawk.libtomahawk.resolver.Query; +import org.tomahawk.tomahawk_android.mediaplayers.TomahawkMediaPlayer; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadManager { + + /* + * Gets the number of available cores + * (not always the same as the maximum number of cores) + */ + private static final int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); + + // Sets the amount of time an idle thread waits before terminating + private static final int KEEP_ALIVE_TIME = 1; + + // Sets the Time Unit to seconds + private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; + + private static class Holder { + + private static final ThreadManager instance = new ThreadManager(); + + } + + private final ThreadPoolExecutor mThreadPool; + + private final ConcurrentHashMap mPlaybackThreadPools + = new ConcurrentHashMap<>(); + + private final Map> mQueryRunnableMap; + + private ThreadManager() { + mQueryRunnableMap = new ConcurrentHashMap<>(); + mThreadPool = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES, + KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, new PriorityBlockingQueue()); + } + + public static ThreadManager get() { + return Holder.instance; + } + + public void execute(TomahawkRunnable r) { + mThreadPool.execute(r); + } + + public void execute(TomahawkRunnable r, Query query) { + Collection runnables = mQueryRunnableMap.get(query); + if (runnables == null) { + runnables = new HashSet<>(); + } + runnables.add(r); + mQueryRunnableMap.put(query, runnables); + mThreadPool.execute(r); + } + + public boolean stop(Query query) { + boolean success = false; + Collection runnables = mQueryRunnableMap.remove(query); + if (runnables != null) { + for (TomahawkRunnable r : runnables) { + mThreadPool.remove(r); + success = true; + } + } + return success; + } + + public void executePlayback(TomahawkMediaPlayer mp, Runnable r) { + ThreadPoolExecutor pool = mPlaybackThreadPools.get(mp); + if (pool == null) { + pool = new ThreadPoolExecutor(1, 1, KEEP_ALIVE_TIME, + KEEP_ALIVE_TIME_UNIT, new LinkedBlockingQueue()); + mPlaybackThreadPools.put(mp, pool); + } + pool.execute(r); + } + + public boolean isActive() { + for (ThreadPoolExecutor pool : mPlaybackThreadPools.values()) { + if (pool.getActiveCount() > 0 || pool.getQueue().size() > 0) { + return true; + } + } + return mThreadPool.getActiveCount() > 0 || mThreadPool.getQueue().size() > 0; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkHttpSender.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkHttpSender.java new file mode 100644 index 000000000..2851c87bc --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkHttpSender.java @@ -0,0 +1,51 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import org.acra.ReportField; +import org.acra.collector.CrashReportData; +import org.acra.sender.HttpSender; +import org.acra.sender.ReportSenderException; +import org.tomahawk.tomahawk_android.dialogs.SendLogConfigDialog; + +import android.content.Context; + +import java.util.Map; + +public class TomahawkHttpSender extends HttpSender { + + public TomahawkHttpSender(Method method, Type type, + Map mapping) { + super(method, type, mapping); + } + + @Override + public void send(Context context, CrashReportData data) throws ReportSenderException { + if (!"org.tomahawk.tomahawk_android".equals(data.getProperty(ReportField.PACKAGE_NAME))) { + return; + } + + if (data.getProperty(ReportField.STACK_TRACE) + .startsWith(SendLogConfigDialog.SendLogException.getDefaultString())) { + // this means that it's a manually send crash report through the "send log"-feature + data.put(ReportField.USER_EMAIL, SendLogConfigDialog.mLastEmail); + data.put(ReportField.USER_COMMENT, SendLogConfigDialog.mLastUsermessage); + } + super.send(context, data); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkRunnable.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkRunnable.java new file mode 100644 index 000000000..bb3409fb6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/TomahawkRunnable.java @@ -0,0 +1,62 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import android.support.annotation.NonNull; + +public abstract class TomahawkRunnable implements Runnable, Comparable { + + public static final int PRIORITY_IS_NOTIFICATION = 500; + + public static final int PRIORITY_IS_VERYHIGH = 200; + + public static final int PRIORITY_IS_INFOSYSTEM_HIGH = 100; + + public static final int PRIORITY_IS_INFOSYSTEM_MEDIUM = 90; + + public static final int PRIORITY_IS_AUTHENTICATING = 50; + + public static final int PRIORITY_IS_REPORTING_LOCALSOURCE = 40; + + public static final int PRIORITY_IS_REPORTING_SUBSCRIPTION = 25; + + public static final int PRIORITY_IS_REPORTING = 20; + + public static final int PRIORITY_IS_RESOLVING = 10; + + public static final int PRIORITY_IS_DATABASEACTION = 5; + + public static final int PRIORITY_IS_INFOSYSTEM_LOW = 4; + + public static final int PRIORITY_IS_REPORTING_WITH_HEADERREQUEST = 0; + + private final int mPriority; + + public TomahawkRunnable(int priority) { + mPriority = priority; + } + + public int getPriority() { + return mPriority; + } + + @Override + public int compareTo(@NonNull TomahawkRunnable other) { + return other.getPriority() - mPriority; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/UnzipUtils.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/UnzipUtils.java new file mode 100644 index 000000000..ba1e1d013 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/UnzipUtils.java @@ -0,0 +1,112 @@ +package org.tomahawk.tomahawk_android.utils; + +import com.squareup.okhttp.Response; + +import org.tomahawk.libtomahawk.utils.NetworkUtils; + +import android.net.Uri; +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * This utility extracts files and directories of a standard zip file to a destination directory. + * + * @author www.codejava.net + */ +public class UnzipUtils { + + private final static String TAG = UnzipUtils.class.getSimpleName(); + + /** + * Size of the buffer (in bytes) to read/write data + */ + private static final int BUFFER_SIZE = 4096; + + /** + * Extracts a zip file specified by the zipFilePath to a directory specified by destDirectory + * (will be created if it doesn't exist) + */ + public static boolean unzip(Uri zipFilePath, String destDirectory) { + File destDir = new File(destDirectory); + if (!destDir.exists()) { + boolean success = destDir.mkdirs(); + if (!success) { + Log.e(TAG, "unzip - Wasn't able to create directory: " + destDirectory); + } + } + ZipInputStream zipIn = null; + Response response = null; + try { + InputStream inputStream; + if (zipFilePath.getScheme().contains("file")) { + inputStream = new FileInputStream(zipFilePath.getPath()); + } else if (zipFilePath.getScheme().contains("http")) { + response = NetworkUtils.httpRequest("GET", zipFilePath.toString(), null, + null, null, null, true, null); + inputStream = response.body().byteStream(); + } else { + Log.e(TAG, "unzip - Can't handle URI scheme"); + return false; + } + zipIn = new ZipInputStream(inputStream); + ZipEntry entry = zipIn.getNextEntry(); + // iterates over entries in the zip file + while (entry != null) { + String filePath = destDirectory + File.separator + entry.getName(); + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + extractFile(zipIn, filePath); + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + boolean success = dir.mkdirs(); + if (!success) { + Log.e(TAG, "unzip - Wasn't able to create directory: " + filePath); + } + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } catch (IOException e) { + Log.e(TAG, "unzip: " + e.getClass() + ": " + e.getLocalizedMessage()); + } finally { + try { + if (zipIn != null) { + zipIn.close(); + } + if (response != null) { + response.body().close(); + } + } catch (IOException e) { + Log.e(TAG, "unzip: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + } + return true; + } + + /** + * Extracts a zip entry (file entry) + */ + private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException { + File dir = new File(filePath).getParentFile(); + boolean success = dir.mkdirs(); + if (!success) { + Log.e(TAG, "extractFile - Wasn't able to create directory: " + filePath); + } + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); + byte[] bytesIn = new byte[BUFFER_SIZE]; + int read; + while ((read = zipIn.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + bos.close(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/utils/WeakReferenceHandler.java b/app/src/main/java/org/tomahawk/tomahawk_android/utils/WeakReferenceHandler.java new file mode 100644 index 000000000..c541aa356 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/utils/WeakReferenceHandler.java @@ -0,0 +1,42 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.utils; + +import android.os.Handler; +import android.os.Looper; + +import java.lang.ref.WeakReference; + +public class WeakReferenceHandler extends Handler { + + private final WeakReference mReference; + + public WeakReferenceHandler(T referencedObject) { + mReference = new WeakReference<>(referencedObject); + } + + public WeakReferenceHandler(Looper looper, T referencedObject) { + super(looper); + mReference = new WeakReference<>(referencedObject); + } + + protected T getReferencedObject() { + return mReference.get(); + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/AlbumArtViewPager.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/AlbumArtViewPager.java new file mode 100644 index 000000000..24d441284 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/AlbumArtViewPager.java @@ -0,0 +1,56 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; + +public class AlbumArtViewPager extends ViewPager { + + private GestureDetector mGestureDetector; + + public AlbumArtViewPager(Context context) { + super(context); + } + + public AlbumArtViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mGestureDetector != null) { + mGestureDetector.onTouchEvent(event); + } + super.onTouchEvent(event); + + return true; + } + + public void setOnGestureListener(GestureDetector.SimpleOnGestureListener listener) { + mGestureDetector = new GestureDetector(getContext(), listener); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/BiDirectionalFrame.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/BiDirectionalFrame.java new file mode 100644 index 000000000..2667ad1be --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/BiDirectionalFrame.java @@ -0,0 +1,121 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +/** + * This class forwards TouchEvents to mForwardView if the GestureDetector has detected a vertical + * scroll. + */ +public class BiDirectionalFrame extends FrameLayout { + + private final GestureDetector mGestureDetector; + + private boolean mVerticallyScrolled; + + private boolean mHorizontallyScrolled; + + private MotionEvent mDownMotionEvent; + + private boolean mTouchCancelled; + + private View mForwardView; + + private class ShouldSwipeDetector extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mVerticallyScrolled = Math.abs(distanceY) >= Math.abs(distanceX); + mHorizontallyScrolled = Math.abs(distanceX) >= Math.abs(distanceY); + return false; + } + } + + public BiDirectionalFrame(Context context) { + super(context); + mGestureDetector = new GestureDetector(context, new ShouldSwipeDetector()); + } + + public BiDirectionalFrame(Context context, AttributeSet attrs) { + super(context, attrs); + mGestureDetector = new GestureDetector(context, new ShouldSwipeDetector()); + } + + public void setForwardView(View forwardView) { + mForwardView = forwardView; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return true; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + if (mForwardView != null) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownMotionEvent = MotionEvent.obtain(event); + mVerticallyScrolled = false; + mHorizontallyScrolled = false; + mTouchCancelled = false; + break; + } + + if (!mVerticallyScrolled && !mHorizontallyScrolled) { + mGestureDetector.onTouchEvent(event); + } + + if (mVerticallyScrolled) { + getParent().requestDisallowInterceptTouchEvent(false); + if (mDownMotionEvent != null) { + super.onTouchEvent(mDownMotionEvent); + mDownMotionEvent = null; + } + super.onTouchEvent(event); + if (!mTouchCancelled) { + mTouchCancelled = true; + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + mForwardView.dispatchTouchEvent(cancel); + cancel.recycle(); + } + } else { + getParent().requestDisallowInterceptTouchEvent(true); + mForwardView.dispatchTouchEvent(event); + getParent().requestDisallowInterceptTouchEvent(true); + } + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + getParent().requestDisallowInterceptTouchEvent(false); + } + return true; + } + + return false; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/BottomCropImageView.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/BottomCropImageView.java new file mode 100644 index 000000000..bb268b3dc --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/BottomCropImageView.java @@ -0,0 +1,64 @@ +package org.tomahawk.tomahawk_android.views; + +import android.content.Context; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class BottomCropImageView extends ImageView { + + public BottomCropImageView(Context context) { + super(context); + setup(); + } + + public BottomCropImageView(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public BottomCropImageView(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + setup(); + } + + private void setup() { + setScaleType(ScaleType.MATRIX); + } + + @Override + protected boolean setFrame(int frameLeft, int frameTop, int frameRight, int frameBottom) { + if (getDrawable() != null) { + float frameWidth = frameRight - frameLeft; + float frameHeight = frameBottom - frameTop; + + float originalImageWidth = (float) getDrawable().getIntrinsicWidth(); + float originalImageHeight = (float) getDrawable().getIntrinsicHeight(); + + float usedScaleFactor = 1; + + if ((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) { + // If frame is bigger than image + // => Crop it, keep aspect ratio and position it at the bottom and center horizontally + + float fitHorizontallyScaleFactor = frameWidth / originalImageWidth; + float fitVerticallyScaleFactor = frameHeight / originalImageHeight; + + usedScaleFactor = Math.max(fitHorizontallyScaleFactor, fitVerticallyScaleFactor); + } + + float newImageWidth = originalImageWidth * usedScaleFactor; + float newImageHeight = originalImageHeight * usedScaleFactor; + + Matrix matrix = getImageMatrix(); + matrix.setScale(usedScaleFactor, usedScaleFactor, 0, + 0); // Replaces the old matrix completly + matrix.postTranslate((frameWidth - newImageWidth) / 2, frameHeight - newImageHeight); + + setImageMatrix(matrix); + } + return super.setFrame(frameLeft, frameTop, frameRight, frameBottom); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/DirectoryChooser.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/DirectoryChooser.java new file mode 100644 index 000000000..75498ea4e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/DirectoryChooser.java @@ -0,0 +1,185 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.libtomahawk.collection.UserCollection; +import org.tomahawk.libtomahawk.database.DatabaseHelper; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.adapters.DirectoryChooserAdapter; + +import android.content.Context; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class DirectoryChooser extends FrameLayout implements + StickyListHeadersListView.OnHeaderClickListener { + + private final static String TAG = DirectoryChooser.class.getSimpleName(); + + private int mDrillDownCount = 0; + + private int mLastDrillDownCount = 0; + + private File mCurrentFolderRoot; + + private DirectoryChooserAdapter mAdapter; + + public DirectoryChooser(Context context) { + super(context); + inflate(getContext(), R.layout.directory_chooser, this); + } + + public DirectoryChooser(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.directory_chooser, this); + } + + public interface DirectoryChooserListener { + + void onDirectoryChecked(File chosenSubFolder, boolean isChecked); + + void onDirectoryBrowsed(File clickedSubFolder); + } + + public void setup() { + mDrillDownCount = 0; + ArrayList storageDirs = new ArrayList<>(); + storageDirs.addAll(UserCollection.getStorageDirectories()); + List mediaDirs = new ArrayList<>(); + for (String dir : storageDirs) { + File f = new File(dir); + if (f.exists()) { + mediaDirs.add(f); + } + } + setup(mediaDirs); + } + + public void setup(File currentFolderRoot) { + mCurrentFolderRoot = currentFolderRoot; + ArrayList folders = new ArrayList<>(); + if (mCurrentFolderRoot.listFiles() != null && mCurrentFolderRoot.listFiles().length > 0) { + for (File file : mCurrentFolderRoot.listFiles()) { + if (file.isDirectory() && !file.isHidden()) { + folders.add(file); + } + } + } + setup(folders); + } + + public void setup(List folders) { + boolean isFirstRoot = mDrillDownCount == 0; + ArrayList dirs + = new ArrayList<>(); + for (File folder : folders) { + DirectoryChooserAdapter.CustomDirectory dir + = new DirectoryChooserAdapter.CustomDirectory(); + dir.file = folder; + try { + dir.isWhitelisted = DatabaseHelper.get() + .isMediaDirWhiteListed(folder.getCanonicalPath()); + dir.isMediaDirComplete = DatabaseHelper.get() + .isMediaDirComplete(folder.getCanonicalPath()); + } catch (IOException e) { + Log.e(TAG, "setup: " + e.getClass() + ": " + e.getLocalizedMessage()); + } + int position; + for (position = 0; position < dirs.size(); position++) { + if (dir.file.getName().compareToIgnoreCase(dirs.get(position).file.getName()) < 0) { + break; + } + } + dirs.add(position, dir); + } + if (mAdapter == null) { + mAdapter = new DirectoryChooserAdapter(LayoutInflater.from(getContext()), isFirstRoot, + dirs, new DirectoryChooserListener() { + @Override + public void onDirectoryChecked(File chosenSubFolder, boolean isChecked) { + try { + if (isChecked) { + DatabaseHelper.get() + .addMediaDir(chosenSubFolder.getCanonicalPath()); + } else { + DatabaseHelper.get() + .removeMediaDir(chosenSubFolder.getCanonicalPath()); + } + } catch (IOException e) { + Log.e(TAG, "onDirectoryChecked: " + e.getClass() + ": " + + e.getLocalizedMessage()); + } + if (mDrillDownCount == 0) { + setup(); + } else { + setup(chosenSubFolder.getParentFile()); + } + } + + @Override + public void onDirectoryBrowsed(File clickedSubFolder) { + for (File file : clickedSubFolder.listFiles()) { + if (file.isDirectory()) { + mDrillDownCount++; + setup(clickedSubFolder); + break; + } + } + } + }); + } else { + mAdapter.update(isFirstRoot, dirs); + } + StickyListHeadersListView listView = + (StickyListHeadersListView) findViewById(R.id.listview); + listView.setOnHeaderClickListener(this); + if (mDrillDownCount == mLastDrillDownCount) { + Parcelable listState = listView.getWrappedList().onSaveInstanceState(); + listView.setAdapter(mAdapter); + listView.getWrappedList().onRestoreInstanceState(listState); + } else { + listView.setAdapter(mAdapter); + } + mLastDrillDownCount = mDrillDownCount; + } + + @Override + public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, + long headerId, boolean currentlySticky) { + if (mCurrentFolderRoot != null && mCurrentFolderRoot.getParentFile() != null) { + mDrillDownCount--; + if (mDrillDownCount > 0) { + setup(mCurrentFolderRoot.getParentFile()); + } else { + setup(); + } + } + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/EqualizerBar.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/EqualizerBar.java new file mode 100644 index 000000000..f37eff40c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/EqualizerBar.java @@ -0,0 +1,100 @@ +/***************************************************************************** + * EqualizerBar.java + ***************************************************************************** + * Copyright © 2013 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.tomahawk_android.R; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +public class EqualizerBar extends LinearLayout { + + private static final int PRECISION = 10; + + private static final int RANGE = 20 * PRECISION; + + private VerticalSeekBar mSeek; + + private TextView mValue; + + private OnEqualizerBarChangeListener listener; + + public interface OnEqualizerBarChangeListener { + + void onProgressChanged(float value); + } + + public EqualizerBar(Context context, float band) { + super(context); + init(context, band); + } + + public EqualizerBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, 0); + } + + private void init(Context context, float band) { + LayoutInflater.from(context).inflate(R.layout.equalizerbar, this, true); + + mSeek = (VerticalSeekBar) findViewById(R.id.equalizer_seek); + mSeek.setMax(2 * RANGE); + mSeek.setProgress(RANGE); + mSeek.setOnSeekBarChangeListener(mSeekListener); + TextView band1 = (TextView) findViewById(R.id.equalizer_band); + band1.setText(band < 999.5f + ? (int) (band + 0.5f) + " Hz" + : (int) (band / 1000.0f + 0.5f) + " kHz"); + mValue = (TextView) findViewById(R.id.equalizer_value); + } + + private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + float value = (progress - RANGE) / (float) PRECISION; + mValue.setText(value + " Db"); + if (listener != null) { + listener.onProgressChanged(value); + } + } + }; + + public void setValue(float value) { + mSeek.setProgress((int) (value * PRECISION + RANGE)); + } + + public void setListener(OnEqualizerBarChangeListener listener) { + this.listener = listener; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/FancyDropDown.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/FancyDropDown.java new file mode 100644 index 000000000..d063a322e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/FancyDropDown.java @@ -0,0 +1,318 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.libtomahawk.collection.Collection; +import org.tomahawk.libtomahawk.resolver.PipeLine; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.resolver.UserCollectionStubResolver; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.listeners.OnSizeChangedListener; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class FancyDropDown extends FrameLayout { + + private int mSelection; + + private List mItemInfos; + + private DropDownListener mListener; + + private boolean mShowing; + + private boolean mIsAnimating; + + private int mItemHeight; + + private SparseArray mItemFrames; + + public String mText; + + private OnSizeChangedListener mOnSizeChangedListener; + + private boolean mCanBeVisible = false; + + public static class DropDownItemInfo { + + public String mText; + + public Resolver mResolver; + + public boolean equals(DropDownItemInfo itemInfo) { + return (mText != null && mText.equals(itemInfo.mText)) + || (mText == null && itemInfo.mText == null) + && mResolver == itemInfo.mResolver; + } + } + + public interface DropDownListener { + + void onDropDownItemSelected(int position); + + void onCancel(); + } + + public FancyDropDown(Context context) { + super(context); + inflate(getContext(), R.layout.fancydropdown, this); + } + + public FancyDropDown(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.fancydropdown, this); + } + + public void setup(int initialSelection, String selectedText, + List dropDownItemInfos, DropDownListener dropDownListener) { + mListener = dropDownListener; + mText = selectedText; + ((TextView) findViewById(R.id.textview_selected)).setText(mText); + mCanBeVisible = true; + + if (dropDownItemInfos != null && dropDownItemInfos.size() > 0) { + // Do we really need to update? Do the new infos differ from the old ones? + boolean differingInfos = mItemInfos == null + || mItemInfos.size() != dropDownItemInfos.size(); + for (int i = 0; !differingInfos && i < mItemInfos.size(); i++) { + if (!mItemInfos.get(i).equals(dropDownItemInfos.get(i))) { + differingInfos = true; + } + } + if (differingInfos) { + mItemInfos = dropDownItemInfos; + mItemFrames = new SparseArray<>(); + LinearLayout itemsContainer = + (LinearLayout) findViewById(R.id.dropdown_items_container); + itemsContainer.removeAllViews(); + updateSelectedItem(initialSelection); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for (int i = 0; i < mItemInfos.size(); i++) { + final LinearLayout item = (LinearLayout) inflater + .inflate(R.layout.fancydropdown_item, this, false); + final TextView textView = (TextView) item.findViewById(R.id.textview); + textView.setText(mItemInfos.get(i).mText.toUpperCase()); + ImageView imageView = (ImageView) item.findViewById(R.id.imageview); + if (mItemInfos.get(i).mResolver != null) { + mItemInfos.get(i).mResolver.loadIconWhite(imageView, 0); + } + + final int position = i; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + hideDropDownList(position); + if (mListener != null) { + mListener.onDropDownItemSelected(position); + } + } + }); + // We need a FrameLayout to hide the item when it's out of the FrameLayout's bounds + FrameLayout frameLayout = new FrameLayout(getContext()); + frameLayout.setLayoutParams( + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + frameLayout.addView(item); + itemsContainer.addView(frameLayout); + mItemFrames.put(position, frameLayout); + } + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(mItemFrames.get(0)) { + @Override + public void run() { + mItemHeight = mItemFrames.get(0).getHeight(); + for (int i = 0; i < mItemFrames.size(); i++) { + mItemFrames.get(i).getChildAt(0).setY(mItemHeight * -1); + } + } + }); + } + } + findViewById(R.id.selected_item_container).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mItemFrames != null && mItemFrames.size() > 1) { + if (mShowing) { + mListener.onCancel(); + hideDropDownList(mSelection); + } else { + showDropDownList(); + } + } + } + }); + } + + private void updateSelectedItem(int newSelection) { + mSelection = newSelection; + ImageView imageView = (ImageView) findViewById(R.id.imageview_selected); + if (mItemInfos != null) { + if (mItemInfos.get(mSelection).mResolver != null) { + mItemInfos.get(mSelection).mResolver.loadIconWhite(imageView, 0); + imageView.setVisibility(VISIBLE); + } else { + imageView.setVisibility(GONE); + } + } else { + imageView.setVisibility(GONE); + } + } + + public void showDropDownList() { + if (!mIsAnimating && !isShowing()) { + animateDropDown(false, new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mShowing = true; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + } + } + + public void hideDropDownList(int selectedItem) { + if (!mIsAnimating && isShowing()) { + updateSelectedItem(selectedItem); + animateDropDown(true, new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mShowing = false; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + } + } + + public boolean isShowing() { + return mShowing; + } + + private void animateDropDown(boolean reverse, Animation.AnimationListener listener) { + int startPos = reverse ? mItemFrames.size() - 1 : 0; + animateDropDownItem(startPos, reverse, 120, listener); + } + + private void animateDropDownItem(final int position, final boolean reverse, final int duration, + final Animation.AnimationListener listener) { + if (reverse ? position >= 0 + : mItemFrames != null && position < mItemFrames.size()) { + View item = mItemFrames.get(position).getChildAt(0); + item.setY(reverse ? 0 : mItemHeight * -1); + final ValueAnimator animator = + ObjectAnimator.ofFloat(item, "y", reverse ? mItemHeight * -1 : 0); + animator.setDuration(duration / mItemFrames.size()); + animator.setInterpolator(new LinearInterpolator()); + animator.start(); + mIsAnimating = true; + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + int newPosition = reverse ? position - 1 : position + 1; + animateDropDownItem(newPosition, reverse, duration, listener); + animator.removeListener(this); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + } else { + mIsAnimating = false; + listener.onAnimationEnd(null); + } + } + + public static List convertToDropDownItemInfo(List collections) { + List dropDownItemInfos + = new ArrayList<>(); + for (Collection collection : collections) { + FancyDropDown.DropDownItemInfo dropDownItemInfo = + new FancyDropDown.DropDownItemInfo(); + if (TomahawkApp.PLUGINNAME_HATCHET.equals(collection.getId())) { + dropDownItemInfo.mText = TomahawkApp.getContext().getString(R.string.all); + } else if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(collection.getId())) { + dropDownItemInfo.mText = TomahawkApp.getContext().getString(R.string.local); + dropDownItemInfo.mResolver = UserCollectionStubResolver.get(); + } else { + Resolver resolver = PipeLine.get().getResolver(collection.getId()); + dropDownItemInfo.mText = resolver.getId(); + dropDownItemInfo.mResolver = resolver; + } + dropDownItemInfos.add(dropDownItemInfo); + } + return dropDownItemInfos; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + if (mCanBeVisible) { + setVisibility(VISIBLE); + } + + if (mOnSizeChangedListener != null) { + mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh); + } + } + + public void setOnSizeChangedListener(OnSizeChangedListener listener) { + mOnSizeChangedListener = listener; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/HatchetLoginRegisterView.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/HatchetLoginRegisterView.java new file mode 100644 index 000000000..f46875ee1 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/HatchetLoginRegisterView.java @@ -0,0 +1,436 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.ui.widgets.ConfigEdittext; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.InputType; +import android.text.TextUtils; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +public class HatchetLoginRegisterView extends LinearLayout { + + private AuthenticatorUtils mAuthenticatorUtils; + + private ProgressBar mProgressBar; + + //So that the user can login by pressing "Enter" or something similar on his keyboard + private final TextView.OnEditorActionListener mOnKeyboardEnterListener + = new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (event == null || actionId == EditorInfo.IME_ACTION_SEARCH + || actionId == EditorInfo.IME_ACTION_DONE + || event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + loginButtonClick(); + } + return false; + } + }; + + private ViewPager mViewPager; + + private TextView mLoginButton; + + private EditText mLoginUsernameEditText; + + private EditText mLoginPasswordEditText; + + private EditText mRegisterUsernameEditText; + + private EditText mRegisterPasswordEditText; + + private EditText mPasswordConfirmationEditText; + + private EditText mMailEditText; + + private ViewPager.OnPageChangeListener mOnPageChangeListener + = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + updateButtonTexts(); + } + + @Override + public void onPageScrollStateChanged(int state) { + switch (mViewPager.getCurrentItem()) { + case 0: + mRegisterUsernameEditText.setText(mLoginUsernameEditText.getText()); + mRegisterPasswordEditText.setText(mLoginPasswordEditText.getText()); + break; + case 1: + mLoginUsernameEditText.setText(mRegisterUsernameEditText.getText()); + mLoginPasswordEditText.setText(mRegisterPasswordEditText.getText()); + break; + } + } + }; + + private class LoginButtonListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + loginButtonClick(); + } + } + + private class LoginRegisterPagerAdapter extends PagerAdapter { + + @Override + public int getCount() { + return 2; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + switch (position) { + case 0: + LinearLayout loginContainer = new LinearLayout(getContext()); + loginContainer.setOrientation(LinearLayout.VERTICAL); + loginContainer.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + mLoginUsernameEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mLoginUsernameEditText + .setHint(mAuthenticatorUtils.getUserIdEditTextHintResId()); + mLoginUsernameEditText.setText(mAuthenticatorUtils.isLoggedIn() + ? mAuthenticatorUtils.getUserName() : ""); + loginContainer.addView(mLoginUsernameEditText); + + mLoginPasswordEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mLoginPasswordEditText.setHint(R.string.login_password); + mLoginPasswordEditText.setTypeface(Typeface.DEFAULT); + mLoginPasswordEditText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + mLoginPasswordEditText + .setTransformationMethod(new PasswordTransformationMethod()); + mLoginPasswordEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + loginContainer.addView(mLoginPasswordEditText); + + if (mAuthenticatorUtils.isLoggedIn()) { + mLoginUsernameEditText.setEnabled(false); + mLoginPasswordEditText.setEnabled(false); + } else { + mLoginUsernameEditText.setEnabled(true); + mLoginPasswordEditText.setEnabled(true); + } + + FrameLayout frameContainer = new FrameLayout(getContext()); + frameContainer.addView(loginContainer); + FrameLayout.LayoutParams frameParams = + (FrameLayout.LayoutParams) loginContainer.getLayoutParams(); + frameParams.gravity = Gravity.CENTER_VERTICAL; + container.addView(frameContainer); + ViewUtils.showSoftKeyboard(mLoginUsernameEditText); + return frameContainer; + case 1: + LinearLayout registerContainer = new LinearLayout(getContext()); + registerContainer.setOrientation(LinearLayout.VERTICAL); + registerContainer.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + mRegisterUsernameEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mRegisterUsernameEditText + .setHint(mAuthenticatorUtils.getUserIdEditTextHintResId()); + mRegisterUsernameEditText.setText(mAuthenticatorUtils.isLoggedIn() + ? mAuthenticatorUtils.getUserName() : ""); + registerContainer.addView(mRegisterUsernameEditText); + + mRegisterPasswordEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mRegisterPasswordEditText.setHint(R.string.login_password); + mRegisterPasswordEditText.setTypeface(Typeface.DEFAULT); + mRegisterPasswordEditText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + mRegisterPasswordEditText + .setTransformationMethod(new PasswordTransformationMethod()); + mRegisterPasswordEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + registerContainer.addView(mRegisterPasswordEditText); + + mPasswordConfirmationEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mPasswordConfirmationEditText.setHint(R.string.login_password_confirmation); + mPasswordConfirmationEditText.setTypeface(Typeface.DEFAULT); + mPasswordConfirmationEditText.setInputType( + InputType.TYPE_TEXT_VARIATION_PASSWORD); + mPasswordConfirmationEditText + .setTransformationMethod(new PasswordTransformationMethod()); + registerContainer.addView(mPasswordConfirmationEditText); + + mMailEditText = (ConfigEdittext) + inflater.inflate(R.layout.config_edittext, container, false); + mMailEditText.setHint(R.string.account_email_label); + mMailEditText.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + mMailEditText.setOnEditorActionListener(mOnKeyboardEnterListener); + registerContainer.addView(mMailEditText); + + if (mAuthenticatorUtils.isLoggedIn()) { + mRegisterUsernameEditText.setEnabled(false); + mRegisterPasswordEditText.setEnabled(false); + mPasswordConfirmationEditText.setEnabled(false); + mMailEditText.setEnabled(false); + } else { + mRegisterUsernameEditText.setEnabled(true); + mRegisterPasswordEditText.setEnabled(true); + mPasswordConfirmationEditText.setEnabled(true); + mMailEditText.setEnabled(true); + } + + frameContainer = new FrameLayout(getContext()); + frameContainer.addView(registerContainer); + frameParams = (FrameLayout.LayoutParams) registerContainer.getLayoutParams(); + frameParams.gravity = Gravity.CENTER_VERTICAL; + container.addView(frameContainer); + return frameContainer; + } + return null; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public String getPageTitle(int position) { + switch (position) { + case 0: + return getContext().getString(R.string.login); + case 1: + return getContext().getString(R.string.register); + } + return ""; + } + + public int getItemPosition(Object object) { + return POSITION_NONE; + } + } + + public HatchetLoginRegisterView(Context context) { + super(context); + inflate(getContext(), R.layout.hatchet_login_register, this); + } + + public HatchetLoginRegisterView(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.hatchet_login_register, this); + } + + public void setup(AuthenticatorUtils authenticatorUtils, ProgressBar progressBar) { + mAuthenticatorUtils = authenticatorUtils; + mProgressBar = progressBar; + + mViewPager = (ViewPager) findViewById(R.id.viewpager); + mViewPager.setAdapter(new LoginRegisterPagerAdapter()); + SimplePagerTabs pagerTabs = + (SimplePagerTabs) findViewById(R.id.simplepagertabs); + pagerTabs.setViewPager(mViewPager); + pagerTabs.setOnPageChangeListener(mOnPageChangeListener); + mLoginButton = (TextView) findViewById(R.id.login_button); + mLoginButton.setOnClickListener(new LoginButtonListener()); + + updateButtonTexts(); + } + + private void loginButtonClick() { + if (mAuthenticatorUtils.isLoggedIn()) { + mProgressBar.setVisibility(VISIBLE); + mAuthenticatorUtils.logout(); + } else { + switch (mViewPager.getCurrentItem()) { + case 0: + attemptLogin(); + break; + case 1: + attemptRegister(); + break; + } + } + } + + /** + * Attempts to sign in or register the account specified by the login form. If there are form + * errors (invalid email, missing fields, etc.), the errors are presented and no actual login + * attempt is made. + */ + private void attemptLogin() { + // Reset errors. + mLoginUsernameEditText.setError(null); + mLoginPasswordEditText.setError(null); + + // Store values at the time of the login attempt. + String mEmail = mLoginUsernameEditText.getText().toString(); + String mPassword = mLoginPasswordEditText.getText().toString(); + + boolean cancel = false; + View focusView = null; + + // Check for a valid email address. + if (TextUtils.isEmpty(mEmail)) { + mLoginUsernameEditText.setError(getContext().getString(R.string.error_field_required)); + focusView = mLoginUsernameEditText; + cancel = true; + } + + // Check for a valid password. + if (TextUtils.isEmpty(mPassword)) { + mLoginPasswordEditText.setError(getContext().getString(R.string.error_field_required)); + focusView = mLoginPasswordEditText; + cancel = true; + } + + if (cancel) { + // There was an error; don't attempt login and focus the first + // form field with an error. + focusView.requestFocus(); + } else { + // Tell the service to login + mAuthenticatorUtils.login(mEmail, mPassword); + mProgressBar.setVisibility(VISIBLE); + } + } + + /** + * Attempts to sign in or register the account specified by the login form. If there are form + * errors (invalid email, missing fields, etc.), the errors are presented and no actual login + * attempt is made. + */ + private void attemptRegister() { + // Reset errors. + mRegisterUsernameEditText.setError(null); + mRegisterPasswordEditText.setError(null); + mPasswordConfirmationEditText.setError(null); + + // Store values at the time of the register attempt. + String username = mRegisterUsernameEditText.getText().toString(); + String password = mRegisterPasswordEditText.getText().toString(); + String passwordConfirmation = mPasswordConfirmationEditText.getText().toString(); + String email = null; + if (!TextUtils.isEmpty(mMailEditText.getText().toString())) { + email = mMailEditText.getText().toString(); + } + + boolean cancel = false; + View focusView = null; + + // Check for a valid username + if (TextUtils.isEmpty(username)) { + mRegisterUsernameEditText + .setError(getContext().getString(R.string.error_field_required)); + focusView = mRegisterUsernameEditText; + cancel = true; + } + + // Check for a valid password. + if (TextUtils.isEmpty(password)) { + mRegisterPasswordEditText + .setError(getContext().getString(R.string.error_field_required)); + focusView = mRegisterPasswordEditText; + cancel = true; + } + + // Check for a valid password confirmation. + if (TextUtils.isEmpty(passwordConfirmation)) { + mPasswordConfirmationEditText.setError(getContext().getString( + R.string.error_field_required)); + focusView = mPasswordConfirmationEditText; + cancel = true; + } + if (!password.equals(passwordConfirmation)) { + mPasswordConfirmationEditText.setError(getContext().getString( + R.string.error_passwords_dont_match)); + focusView = mPasswordConfirmationEditText; + cancel = true; + } + + if (cancel) { + // There was an error; don't attempt register and focus the first + // form field with an error. + focusView.requestFocus(); + } else { + // Tell the service to register + mAuthenticatorUtils.register(username, password, email); + mProgressBar.setVisibility(VISIBLE); + } + } + + /** + * Update the texts of all buttons. Depends on whether or not the user is logged in. + */ + public void updateButtonTexts() { + if (mAuthenticatorUtils.isLoggedIn()) { + mLoginButton.setTextColor(getResources().getColor(R.color.primary_textcolor_inverted)); + mLoginButton.setBackgroundResource( + R.drawable.selectable_background_tomahawk_red_filled); + mLoginButton.setText(R.string.logout); + } else { + mLoginButton.setTextColor(getResources().getColor(R.color.tomahawk_red)); + mLoginButton.setBackgroundResource(R.drawable.selectable_background_tomahawk_red); + switch (mViewPager.getCurrentItem()) { + case 0: + mLoginButton.setText(R.string.login); + break; + case 1: + mLoginButton.setText(R.string.register_and_login); + break; + } + } + } + + public void onConfigTestResult(Object component, int type, String message) { + if (mAuthenticatorUtils == component) { + updateButtonTexts(); + mViewPager.getAdapter().notifyDataSetChanged(); + mProgressBar.setVisibility(GONE); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/PageIndicator.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/PageIndicator.java new file mode 100644 index 000000000..2bede8fc4 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/PageIndicator.java @@ -0,0 +1,223 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.adapters.TomahawkPagerAdapter; +import org.tomahawk.tomahawk_android.fragments.PagerFragment; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.listeners.OnSizeChangedListener; + +import android.content.Context; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class PageIndicator extends LinearLayout implements ViewPager.OnPageChangeListener { + + private ViewPager mViewPager; + + private List mFragmentInfosList; + + private final List mItems = new ArrayList<>(); + + private View mRootview; + + private Selector mSelector; + + private String mSelectorPosStorageKey; + + private OnSizeChangedListener mOnSizeChangedListener; + + public PageIndicator(Context context) { + super(context); + } + + public PageIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setup(ViewPager viewPager, List fragmentInfosList, + View rootView, Selector selector, String selectorPosStorageKey) { + mViewPager = viewPager; + mFragmentInfosList = fragmentInfosList; + mRootview = rootView; + mSelector = selector; + mSelectorPosStorageKey = selectorPosStorageKey; + populate(); + } + + private void populate() { + removeAllViews(); + mItems.clear(); + for (int i = 0; i < mViewPager.getAdapter().getCount(); i++) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + final View item = inflater.inflate(R.layout.page_indicator_item, this, false); + final TextView textView = (TextView) item.findViewById(R.id.textview); + textView.setText(mViewPager.getAdapter().getPageTitle(i)); + final int j = i; + if (mFragmentInfosList.get(i).size() > 1) { + final ImageView arrow = (ImageView) item.findViewById(R.id.arrow); + arrow.setVisibility(VISIBLE); + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!mSelector.isListShowing()) { + rotateArrow(arrow, false); + + mViewPager.setCurrentItem(j); + + final List fragmentInfos = + mFragmentInfosList.get(j).getFragmentInfos(); + Selector.SelectorListener selectorListener + = new Selector.SelectorListener() { + @Override + public void onSelectorItemSelected(int position) { + rotateArrow(arrow, true); + + FragmentInfo selectedItem = fragmentInfos.get(position); + ((TomahawkPagerAdapter) mViewPager.getAdapter()).changeFragment( + j, selectedItem); + TextView textView = + (TextView) mItems.get(j).findViewById(R.id.textview); + textView.setText(selectedItem.mTitle); + ImageView imageView = + (ImageView) item.findViewById(R.id.imageview); + imageView.setImageResource(selectedItem.mIconResId); + } + + @Override + public void onCancel() { + rotateArrow(arrow, true); + } + }; + mSelector.setup(fragmentInfos, selectorListener, mRootview, + mSelectorPosStorageKey); + mSelector.showSelectorList(); + } else { + rotateArrow(arrow, true); + + mSelector.hideSelectorList(); + } + } + }); + } else { + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mSelector.hideSelectorList(); + mViewPager.setCurrentItem(j); + } + }); + } + if (mFragmentInfosList.get(i).getCurrentFragmentInfo().mIconResId > 0) { + ImageView imageView = (ImageView) item.findViewById(R.id.imageview); + imageView.setVisibility(VISIBLE); + imageView.setImageResource( + mFragmentInfosList.get(i).getCurrentFragmentInfo().mIconResId); + } + if (i != 0) { + View spacer = inflater.inflate(R.layout.page_indicator_spacer, this, false); + addView(spacer); + } + addView(item); + mItems.add(item); + updateColors(mViewPager.getCurrentItem()); + } + } + + private void rotateArrow(View arrow, boolean reverse) { + RotateAnimation rotate; + if (reverse) { + rotate = new RotateAnimation(180, 360, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f); + } else { + rotate = new RotateAnimation(360, 180, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f); + } + rotate.setDuration(AnimationUtils.DURATION_ARROWROTATE); + arrow.startAnimation(rotate); + rotate.setFillAfter(true); + } + + private void updateColors(int position) { + for (int i = 0; i < mItems.size(); i++) { + TextView textView = (TextView) mItems.get(i).findViewById(R.id.textview); + ImageView imageView = (ImageView) mItems.get(i).findViewById(R.id.imageview); + ImageView arrow = (ImageView) mItems.get(i).findViewById(R.id.arrow); + if (i == position) { + textView.setTextColor( + getResources().getColor(R.color.primary_textcolor_inverted)); + imageView.clearColorFilter(); + arrow.clearColorFilter(); + } else { + textView.setTextColor( + getResources().getColor(R.color.tertiary_textcolor_inverted)); + ColorFilter grayOutFilter = new PorterDuffColorFilter( + TomahawkApp.getContext().getResources() + .getColor(R.color.tertiary_textcolor_inverted), + PorterDuff.Mode.MULTIPLY); + imageView.setColorFilter(grayOutFilter); + arrow.setColorFilter(grayOutFilter); + } + } + } + + @Override + public void onPageScrolled(int i, float v, int i2) { + } + + @Override + public void onPageSelected(int position) { + updateColors(position); + } + + @Override + public void onPageScrollStateChanged(int i) { + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + if (mOnSizeChangedListener != null) { + mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh); + } + } + + public void setOnSizeChangedListener(OnSizeChangedListener listener) { + mOnSizeChangedListener = listener; + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackFragmentFrame.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackFragmentFrame.java new file mode 100644 index 000000000..53219dfaa --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackFragmentFrame.java @@ -0,0 +1,130 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.widget.FrameLayout; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class PlaybackFragmentFrame extends FrameLayout { + + private SlidingUpPanelLayout mPanelLayout; + + private StickyListHeadersListView mListView; + + private final GestureDetector mGestureDetector; + + private boolean mVerticallyScrolled; + + private MotionEvent mDownMotionEvent; + + private boolean mTouchCancelled; + + /** + * Class to extend a {@link android.view.GestureDetector.SimpleOnGestureListener}, so that we + * can apply our logic to manually solve the TouchEvent conflict. + */ + private class ShouldSwipeDetector extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return Math.abs(distanceY) >= Math.abs(distanceX) && distanceY < 0; + } + } + + public PlaybackFragmentFrame(Context context) { + super(context); + mGestureDetector = new GestureDetector(context, new ShouldSwipeDetector()); + } + + public PlaybackFragmentFrame(Context context, AttributeSet attrs) { + super(context, attrs); + mGestureDetector = new GestureDetector(context, new ShouldSwipeDetector()); + } + + public void setPanelLayout(SlidingUpPanelLayout panelLayout) { + mPanelLayout = panelLayout; + } + + public void setListView(StickyListHeadersListView listView) { + mListView = listView; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return true; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownMotionEvent = MotionEvent.obtain(event); + mVerticallyScrolled = false; + mTouchCancelled = false; + break; + } + + if (!mVerticallyScrolled) { + mVerticallyScrolled = mGestureDetector.onTouchEvent(event); + } + + if ((mPanelLayout != null + && mPanelLayout.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) + || (mVerticallyScrolled && isListViewScrolledUp())) { + getParent().requestDisallowInterceptTouchEvent(false); + if (mDownMotionEvent != null) { + super.onTouchEvent(mDownMotionEvent); + mDownMotionEvent = null; + } + super.onTouchEvent(event); + ensureTouchCancel(event); + } else { + getParent().requestDisallowInterceptTouchEvent(true); + if (getChildAt(0) != null) { + getChildAt(0).dispatchTouchEvent(event); + } + getParent().requestDisallowInterceptTouchEvent(true); + } + + return true; + } + + private boolean isListViewScrolledUp() { + return mListView != null && mListView.getFirstVisiblePosition() == 0 + && (mListView.getListChildAt(0) == null + || mListView.getListChildAt(0).getTop() >= 0); + } + + private void ensureTouchCancel(MotionEvent event) { + if (!mTouchCancelled) { + mTouchCancelled = true; + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + getChildAt(0).dispatchTouchEvent(cancel); + cancel.recycle(); + } + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackPanel.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackPanel.java new file mode 100644 index 000000000..5e4c24eea --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/PlaybackPanel.java @@ -0,0 +1,574 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import com.github.rahatarmanahmed.cpv.CircularProgressView; +import com.nineoldandroids.animation.Keyframe; +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.PropertyValuesHolder; +import com.nineoldandroids.animation.ValueAnimator; + +import org.tomahawk.libtomahawk.collection.StationPlaylist; +import org.tomahawk.libtomahawk.resolver.Resolver; +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.TomahawkApp; +import org.tomahawk.tomahawk_android.services.PlaybackService; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.PlaybackManager; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; +import org.tomahawk.tomahawk_android.utils.ProgressBarUpdater; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.TransitionDrawable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.HashSet; +import java.util.Set; + +public class PlaybackPanel extends FrameLayout { + + public static final String COMPLETION_STRING_DEFAULT = "-:--"; + + private FrameLayout mTextViewContainer; + + private View mPanelContainer; + + private View mStationContainer; + + private View mStationContainerInner; + + private FrameLayout mArtistNameButton; + + private TextView mArtistTextView; + + private TextView mTrackTextView; + + private TextView mCompletionTimeTextView; + + private TextView mCurrentTimeTextView; + + private TextView mSeekTimeTextView; + + private TextView mStationTextView; + + private ImageView mResolverImageView; + + private ImageView mPlayButton; + + private ImageView mPauseButton; + + private ProgressBar mProgressBar; + + private View mProgressBarThumb; + + private float mLastThumbPosition = -1f; + + private boolean mAbortSeeking; + + private FrameLayout mPlayPauseButtonContainer; + + private CircularProgressView mPlayPauseButton; + + private ValueAnimator mStationContainerAnimation; + + private final Set mAnimators = new HashSet<>(); + + private int mLastPlayTime = 0; + + private boolean mInitialized = false; + + private MediaControllerCompat mMediaController; + + private PlaybackManager mPlaybackManager; + + private ProgressBarUpdater mProgressBarUpdater; + + private long mCurrentDuration; + + private OnLayoutChangeListener mStationLayoutChangeListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + setupStationContainerAnimation(); + } + }; + + public PlaybackPanel(Context context) { + super(context); + inflate(getContext(), R.layout.playback_panel, this); + init(); + } + + public PlaybackPanel(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.playback_panel, this); + init(); + } + + private void init() { + mTextViewContainer = (FrameLayout) findViewById(R.id.textview_container); + mPanelContainer = findViewById(R.id.panel_container); + mStationContainer = findViewById(R.id.station_container); + mStationContainerInner = findViewById(R.id.station_container_inner); + mStationContainerInner.addOnLayoutChangeListener(mStationLayoutChangeListener); + mArtistNameButton = (FrameLayout) mTextViewContainer.findViewById(R.id.artist_name_button); + mArtistTextView = (TextView) mArtistNameButton.findViewById(R.id.artist_textview); + mTrackTextView = (TextView) mTextViewContainer.findViewById(R.id.track_textview); + mCompletionTimeTextView = (TextView) findViewById(R.id.completiontime_textview); + mCurrentTimeTextView = (TextView) findViewById(R.id.currenttime_textview); + mSeekTimeTextView = (TextView) findViewById(R.id.seektime_textview); + mStationTextView = (TextView) findViewById(R.id.station_textview); + mResolverImageView = (ImageView) findViewById(R.id.resolver_imageview); + mPlayButton = (ImageView) findViewById(R.id.play_button); + mPauseButton = (ImageView) findViewById(R.id.pause_button); + mProgressBar = (ProgressBar) findViewById(R.id.progressbar); + mProgressBarThumb = findViewById(R.id.progressbar_thumb); + mPlayPauseButtonContainer = + (FrameLayout) findViewById(R.id.circularprogressbar_container); + mPlayPauseButton = (CircularProgressView) + mPlayPauseButtonContainer.findViewById(R.id.circularprogressbar); + + mProgressBarUpdater = new ProgressBarUpdater( + new ProgressBarUpdater.UpdateProgressRunnable() { + @Override + public void updateProgress(PlaybackStateCompat playbackState, long duration) { + if (playbackState == null) { + return; + } + long currentPosition = playbackState.getPosition(); + if (playbackState.getState() != PlaybackStateCompat.STATE_PAUSED) { + // Calculate the elapsed time between the last position update and now and unless + // paused, we can assume (delta * speed) + current position is approximately the + // latest position. This ensure that we do not repeatedly call the getPlaybackState() + // on MediaControllerCompat. + long timeDelta = SystemClock.elapsedRealtime() - + playbackState.getLastPositionUpdateTime(); + currentPosition += (int) timeDelta * playbackState.getPlaybackSpeed(); + } + mProgressBar + .setProgress((int) ((float) currentPosition / duration * 10000)); + mPlayPauseButton.setProgress((float) currentPosition / duration * 1000); + mCurrentTimeTextView.setText(ViewUtils.durationToString(currentPosition)); + } + }); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + return false; + } + + public void setup(final boolean isPanelExpanded) { + mInitialized = true; + + mPlayPauseButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mMediaController != null) { + int playState = mMediaController.getPlaybackState().getState(); + if (playState == PlaybackStateCompat.STATE_PAUSED + || playState == PlaybackStateCompat.STATE_NONE) { + mMediaController.getTransportControls().play(); + } else if (playState == PlaybackStateCompat.STATE_PLAYING) { + mMediaController.getTransportControls().pause(); + mMediaController.getTransportControls() + .sendCustomAction(PlaybackService.ACTION_STOP_NOTIFICATION, null); + } + } + } + }); + + mPlayPauseButton.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + PreferenceUtils.edit() + .putBoolean(PreferenceUtils.COACHMARK_SEEK_DISABLED, true) + .apply(); + View coachMark = ViewUtils.ensureInflation(PlaybackPanel.this, + R.id.playbackpanel_seek_coachmark_stub, R.id.playbackpanel_seek_coachmark); + coachMark.setVisibility(GONE); + if (!isPanelExpanded || getResources().getBoolean(R.bool.is_landscape)) { + AnimationUtils.fade(mTextViewContainer, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + } + AnimationUtils.fade(mPlayPauseButtonContainer, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mResolverImageView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mCompletionTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mProgressBarThumb, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mCurrentTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mSeekTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mProgressBar, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + + mPlayPauseButton.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (!isPanelExpanded || getResources() + .getBoolean(R.bool.is_landscape)) { + AnimationUtils.fade(mTextViewContainer, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + } + AnimationUtils.fade(mPlayPauseButtonContainer, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mResolverImageView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mCompletionTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, true, true); + AnimationUtils.fade(mProgressBarThumb, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mCurrentTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mSeekTimeTextView, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + AnimationUtils.fade(mProgressBar, + AnimationUtils.DURATION_PLAYBACKSEEKMODE, false, true); + mPlayPauseButton.setOnTouchListener(null); + if (!mAbortSeeking) { + int seekTime = (int) ((mLastThumbPosition - mProgressBar.getX()) + / mProgressBar.getWidth() * mCurrentDuration); + mMediaController.getTransportControls().seekTo(seekTime); + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + float eventX = event.getX(); + float progressBarX = mProgressBar.getX(); + float finalX; + if (eventX > mProgressBar.getWidth() + progressBarX) { + // Only fade out thumb if eventX is above the threshold + int threshold = getResources().getDimensionPixelSize( + R.dimen.playback_panel_seekbar_threshold_end); + mAbortSeeking = eventX > mProgressBar.getWidth() + progressBarX + + threshold; + finalX = mProgressBar.getWidth() + progressBarX; + } else if (eventX < progressBarX) { + // Only fade out thumb if eventX is below the threshold + int threshold = getResources().getDimensionPixelSize( + R.dimen.playback_panel_seekbar_threshold_start); + mAbortSeeking = eventX < progressBarX - threshold; + finalX = progressBarX; + } else { + mAbortSeeking = false; + finalX = eventX; + } + if (mAbortSeeking) { + AnimationUtils.fade(mProgressBarThumb, + AnimationUtils.DURATION_PLAYBACKSEEKMODE_ABORT, false, + true); + } else { + AnimationUtils.fade(mProgressBarThumb, + AnimationUtils.DURATION_PLAYBACKSEEKMODE_ABORT, true, + true); + } + mLastThumbPosition = finalX; + mProgressBarThumb.setX(finalX); + int seekTime = (int) + ((finalX - mProgressBar.getX()) / mProgressBar.getWidth() + * mCurrentDuration); + mSeekTimeTextView.setText(ViewUtils.durationToString(seekTime)); + } + return false; + } + }); + return true; + } + }); + + setupAnimations(); + } + + public void setMediaController(MediaControllerCompat mediaController) { + mMediaController = mediaController; + String playbackManagerId = mediaController.getExtras() + .getString(PlaybackService.EXTRAS_KEY_PLAYBACKMANAGER); + mPlaybackManager = PlaybackManager.getByKey(playbackManagerId); + if (mediaController.getMetadata() != null) { + updateMetadata(mediaController.getMetadata()); + } + updatePlaybackState(mediaController.getPlaybackState()); + } + + public void updateMetadata(MediaMetadataCompat metadata) { + mCurrentDuration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); + mProgressBarUpdater.setCurrentDuration(mCurrentDuration); + mCurrentTimeTextView.setText(ViewUtils.durationToString(0)); + updateTextViewCompleteTime(); + updateText(); + updateImageViews(); + } + + public void updatePlaybackState(PlaybackStateCompat playbackState) { + if (mInitialized) { + mProgressBarUpdater.setPlaybackState(playbackState); + if (playbackState.getState() == PlaybackStateCompat.STATE_PLAYING) { + if (mPlayPauseButton.isIndeterminate()) { + mPlayPauseButton.setIndeterminate(false); + mPlayPauseButton.setColor(getResources().getColor(android.R.color.white)); + mPlayPauseButton.resetAnimation(); + } + mPauseButton.setVisibility(VISIBLE); + mPlayButton.setVisibility(GONE); + mProgressBarUpdater.scheduleSeekbarUpdate(); + } else if (playbackState.getState() == PlaybackStateCompat.STATE_PAUSED) { + if (mPlayPauseButton.isIndeterminate()) { + mPlayPauseButton.setIndeterminate(false); + mPlayPauseButton.setColor(getResources().getColor(android.R.color.white)); + mPlayPauseButton.resetAnimation(); + } + mPauseButton.setVisibility(GONE); + mPlayButton.setVisibility(VISIBLE); + mProgressBarUpdater.stopSeekbarUpdate(); + } else if (playbackState.getState() == PlaybackStateCompat.STATE_BUFFERING) { + if (!mPlayPauseButton.isIndeterminate()) { + mPlayPauseButton.setIndeterminate(true); + mPlayPauseButton.setColor(getResources().getColor(R.color.tomahawk_red)); + mPlayPauseButton.startAnimation(); + } + mProgressBarUpdater.stopSeekbarUpdate(); + } + } + } + + private void updateText() { + if (mPlaybackManager.getCurrentQuery() != null) { + mArtistTextView.setText(mPlaybackManager.getCurrentQuery().getArtist().getPrettyName()); + mTrackTextView.setText(mPlaybackManager.getCurrentQuery().getPrettyName()); + } else { + mArtistTextView.setText(null); + mTrackTextView.setText(null); + } + if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + if (mPlaybackManager.getCurrentQuery() == null) { + MediaMetadataCompat metadata = mMediaController.getMetadata(); + if (metadata != null) { + String displayTitle = + metadata.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE); + if (displayTitle != null) { + mTrackTextView.setText(displayTitle); + } + } + } + mStationContainer.setVisibility(VISIBLE); + mStationTextView.setText(mPlaybackManager.getPlaylist().getName()); + } else { + mStationContainer.setVisibility(INVISIBLE); + } + + } + + private void updateImageViews() { + if (mPlaybackManager.getCurrentQuery() != null + && mPlaybackManager.getCurrentQuery().getPreferredTrackResult() != null) { + mResolverImageView.setVisibility(VISIBLE); + Resolver resolver = + mPlaybackManager.getCurrentQuery().getPreferredTrackResult().getResolvedBy(); + if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(resolver.getId())) { + resolver.loadIconWhite(mResolverImageView, 0); + } else { + resolver.loadIcon(mResolverImageView, false); + } + } else { + mResolverImageView.setVisibility(INVISIBLE); + } + } + + /** + * Updates the textview that shows the duration of the current track + */ + private void updateTextViewCompleteTime() { + if (mPlaybackManager.getCurrentTrack() != null + && mPlaybackManager.getCurrentTrack().getDuration() > 0) { + mCompletionTimeTextView.setText( + ViewUtils.durationToString(mPlaybackManager.getCurrentTrack().getDuration())); + } else { + mCompletionTimeTextView.setText(COMPLETION_STRING_DEFAULT); + } + } + + private void setupAnimations() { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(this) { + @Override + public void run() { + mAnimators.clear(); + // get relevant dimension sizes first + Resources resources = TomahawkApp.getContext().getResources(); + int panelHeight = resources.getDimensionPixelSize( + R.dimen.playback_panel_height); + int paddingSmall = resources.getDimensionPixelSize( + R.dimen.padding_small); + int paddingLarge = resources.getDimensionPixelSize( + R.dimen.padding_large); + int panelBottom = resources.getDimensionPixelSize( + R.dimen.playback_clear_space_bottom); + int headerClearSpace = resources.getDimensionPixelSize( + R.dimen.header_clear_space_nonscrollable_playback); + boolean isLandscape = resources.getBoolean(R.bool.is_landscape); + + // Setup mTextViewContainer animation + Keyframe kfY0 = Keyframe.ofFloat(0f, + getHeight() - mTextViewContainer.getHeight() / 2 - panelHeight / 2); + Keyframe kfY1 = Keyframe.ofFloat(0.5f, + isLandscape ? + getHeight() + paddingLarge - panelBottom - mTextViewContainer + .getHeight() + : getHeight() + paddingSmall - panelBottom); + Keyframe kfY2 = Keyframe.ofFloat(1f, + headerClearSpace / 2 - mTextViewContainer.getHeight() / 2); + PropertyValuesHolder pvhY = + PropertyValuesHolder.ofKeyframe("y", kfY0, kfY1, kfY2); + Keyframe kfScale0 = Keyframe.ofFloat(0f, 1f); + Keyframe kfScale1 = Keyframe.ofFloat(0.5f, isLandscape ? 1.25f : 1.5f); + Keyframe kfScale2 = Keyframe.ofFloat(1f, isLandscape ? 1.25f : 1.5f); + PropertyValuesHolder pvhScaleY = + PropertyValuesHolder.ofKeyframe("scaleY", kfScale0, kfScale1, kfScale2); + PropertyValuesHolder pvhScaleX = + PropertyValuesHolder.ofKeyframe("scaleX", kfScale0, kfScale1, kfScale2); + ValueAnimator animator = ObjectAnimator + .ofPropertyValuesHolder(mTextViewContainer, pvhY, + pvhScaleX, pvhScaleY).setDuration(20000); + animator.setInterpolator(new LinearInterpolator()); + animator.setCurrentPlayTime(mLastPlayTime); + mAnimators.add(animator); + + // Setup mPanelContainer animation + kfY0 = Keyframe.ofFloat(0f, getHeight() - mPanelContainer.getHeight()); + kfY1 = Keyframe.ofFloat(0.5f, getHeight() - mPanelContainer.getHeight()); + kfY2 = Keyframe.ofFloat(1f, headerClearSpace - mPanelContainer.getHeight()); + pvhY = PropertyValuesHolder.ofKeyframe("y", kfY0, kfY1, kfY2); + animator = ObjectAnimator + .ofPropertyValuesHolder(mPanelContainer, pvhY).setDuration(20000); + animator.setInterpolator(new LinearInterpolator()); + animator.setCurrentPlayTime(mLastPlayTime); + mAnimators.add(animator); + + // Setup mTextViewContainer backgroundColor alpha animation + Keyframe kfColor1 = Keyframe.ofInt(0f, 0x0); + Keyframe kfColor2 = Keyframe.ofInt(0.5f, isLandscape ? 0xFF : 0x0); + Keyframe kfColor3 = Keyframe.ofInt(1f, 0xFF); + PropertyValuesHolder pvhColor = PropertyValuesHolder + .ofKeyframe("color", kfColor1, kfColor2, kfColor3); + animator = ValueAnimator.ofPropertyValuesHolder(pvhColor).setDuration(20000); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mTextViewContainer.findViewById(R.id.textview_container_inner) + .getBackground().setAlpha((Integer) animation.getAnimatedValue()); + } + }); + animator.setInterpolator(new LinearInterpolator()); + animator.setCurrentPlayTime(mLastPlayTime); + mAnimators.add(animator); + + // Setup mPanelContainer background fade animation + Keyframe kfBgAlpha0 = Keyframe.ofInt(0f, 0); + Keyframe kfBgAlpha1 = Keyframe.ofInt(0.5f, 0); + Keyframe kfBgAlpha2 = Keyframe.ofInt(1f, 255); + PropertyValuesHolder pvhBgAlpha = PropertyValuesHolder + .ofKeyframe("alpha", kfBgAlpha0, kfBgAlpha1, kfBgAlpha2); + animator = ObjectAnimator.ofPropertyValuesHolder(mPanelContainer.getBackground(), + pvhBgAlpha).setDuration(20000); + animator.setInterpolator(new LinearInterpolator()); + animator.setCurrentPlayTime(mLastPlayTime); + mAnimators.add(animator); + + setupStationContainerAnimation(); + } + }); + } + + public void setupStationContainerAnimation() { + Resources resources = TomahawkApp.getContext().getResources(); + int resolverIconSize = resources.getDimensionPixelSize( + R.dimen.playback_panel_resolver_icon_size); + int padding = resources.getDimensionPixelSize( + R.dimen.padding_small); + + // Setup mStationContainer animation + Keyframe kfX0 = Keyframe.ofFloat(0f, + mStationContainer.getWidth() - resolverIconSize); + Keyframe kfX1 = Keyframe.ofFloat(0.5f, + Math.max(resolverIconSize + padding, + mStationContainer.getWidth() / 2 - mStationContainerInner.getWidth() / 2)); + Keyframe kfX2 = Keyframe.ofFloat(1f, + mStationContainer.getWidth() - resolverIconSize); + PropertyValuesHolder pvhX = PropertyValuesHolder.ofKeyframe("x", kfX0, kfX1, kfX2); + ValueAnimator animator = ObjectAnimator + .ofPropertyValuesHolder(mStationContainerInner, pvhX).setDuration(20000); + animator.setInterpolator(new LinearInterpolator()); + animator.setCurrentPlayTime(mLastPlayTime); + if (mStationContainerAnimation != null) { + mAnimators.remove(mStationContainerAnimation); + } + mStationContainerAnimation = animator; + mAnimators.add(mStationContainerAnimation); + } + + public void hideStationContainer() { + if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + AnimationUtils + .fade(mStationContainer, AnimationUtils.DURATION_PLAYBACKTOPPANEL, false, true); + } + } + + public void showStationContainer() { + if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { + AnimationUtils + .fade(mStationContainer, AnimationUtils.DURATION_PLAYBACKTOPPANEL, true, true); + } + } + + public void animate(int position) { + mLastPlayTime = position; + for (ValueAnimator animator : mAnimators) { + if (animator != null && position != animator.getCurrentPlayTime()) { + animator.setCurrentPlayTime(position); + } + } + } + + public void showButtons() { + mArtistNameButton.setClickable(true); + TransitionDrawable drawable = (TransitionDrawable) mArtistNameButton.getBackground(); + drawable.startTransition(AnimationUtils.DURATION_CONTEXTMENU); + } + + public void hideButtons() { + mArtistNameButton.setClickable(false); + TransitionDrawable drawable = (TransitionDrawable) mArtistNameButton.getBackground(); + drawable.reverseTransition(AnimationUtils.DURATION_CONTEXTMENU); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/Selector.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/Selector.java new file mode 100644 index 000000000..cca81d38e --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/Selector.java @@ -0,0 +1,198 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2014, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsManager; +import org.tomahawk.libtomahawk.infosystem.charts.ScriptChartsProvider; +import org.tomahawk.tomahawk_android.R; +import org.tomahawk.tomahawk_android.fragments.ChartsPagerFragment; +import org.tomahawk.tomahawk_android.utils.AnimationUtils; +import org.tomahawk.tomahawk_android.utils.BlurTransformation; +import org.tomahawk.tomahawk_android.utils.FragmentInfo; +import org.tomahawk.tomahawk_android.utils.PreferenceUtils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.ScaleAnimation; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.List; + +public class Selector extends FrameLayout { + + private List mFragmentInfos; + + private SelectorListener mSelectorListener; + + private View mRootView; + + private String mSelectorPosStorageKey; + + private boolean mListShowing; + + public interface SelectorListener { + + void onSelectorItemSelected(int position); + + void onCancel(); + } + + public Selector(Context context) { + super(context); + inflate(getContext(), R.layout.selector, this); + } + + public Selector(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.selector, this); + } + + public void setup(List selectorItems, + SelectorListener selectorListener, View rootView, String selectorPosStorageKey) { + mFragmentInfos = selectorItems; + mSelectorListener = selectorListener; + mRootView = rootView; + mSelectorPosStorageKey = selectorPosStorageKey; + } + + public void showSelectorList() { + if (!isListShowing()) { + setClickable(true); + mListShowing = true; + Bitmap bm = Bitmap.createBitmap(mRootView.getWidth(), + mRootView.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bm); + mRootView.draw(canvas); + bm = Bitmap.createScaledBitmap(bm, bm.getWidth() / 4, + bm.getHeight() / 4, true); + bm = new BlurTransformation(getContext(), 25).transform(bm); + final ImageView bgImageView = (ImageView) findViewById(R.id.background); + bgImageView.setImageBitmap(bm); + + final LinearLayout selectorFrame = (LinearLayout) findViewById(R.id.selector_frame); + selectorFrame.removeAllViews(); + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for (int i = 0; i < mFragmentInfos.size(); i++) { + LinearLayout item = (LinearLayout) inflater.inflate(R.layout.selectorfragment_item, + selectorFrame, false); + final TextView textView = (TextView) item.findViewById(R.id.textview); + textView.setText(mFragmentInfos.get(i).mTitle.toUpperCase()); + ImageView imageView = (ImageView) item.findViewById(R.id.imageview); + if (mFragmentInfos.get(i).mBundle + .containsKey(ChartsPagerFragment.CHARTSPROVIDER_ID)) { + String chartsProviderId = mFragmentInfos.get(i).mBundle + .getString(ChartsPagerFragment.CHARTSPROVIDER_ID); + ScriptChartsProvider provider = + ScriptChartsManager.get().getScriptChartsProvider(chartsProviderId); + provider.getScriptAccount().loadIconWhite(imageView, 0); + } else { + imageView.setImageResource(mFragmentInfos.get(i).mIconResId); + } + + final int position = i; + item.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideSelectorList(); + mSelectorListener.onSelectorItemSelected(position); + + if (mSelectorPosStorageKey != null) { + int initialPos = PreferenceUtils.getInt(mSelectorPosStorageKey, 0); + if (initialPos != position) { + PreferenceUtils.edit() + .putInt(mSelectorPosStorageKey, position) + .apply(); + } + } + } + }); + selectorFrame.addView(item); + } + //Set up cancel button + LinearLayout item = (LinearLayout) inflater.inflate(R.layout.selectorfragment_item, + selectorFrame, false); + final TextView textView = (TextView) item.findViewById(R.id.textview); + textView.setText(getResources().getString(R.string.cancel).toUpperCase()); + ImageView imageView = (ImageView) item.findViewById(R.id.imageview); + imageView.setImageResource(R.drawable.ic_navigation_close); + item.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideSelectorList(); + mSelectorListener.onCancel(); + } + }); + selectorFrame.addView(item); + AnimationUtils.fade(bgImageView, 120, true); + AnimationUtils.fade(findViewById(R.id.darkening_background), 120, true); + animateScale(selectorFrame, false, null); + } + } + + public void hideSelectorList() { + if (isListShowing()) { + setClickable(false); + mListShowing = false; + AnimationUtils.fade(findViewById(R.id.darkening_background), 120, false); + AnimationUtils.fade(findViewById(R.id.background), 120, false); + final LinearLayout selectorFrame = (LinearLayout) findViewById(R.id.selector_frame); + animateScale(selectorFrame, true, new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + selectorFrame.removeAllViews(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + } + } + + public boolean isListShowing() { + return mListShowing; + } + + private void animateScale(View view, boolean reverse, Animation.AnimationListener listener) { + ScaleAnimation animation; + if (reverse) { + animation = new ScaleAnimation(1f, 0.5f, 1f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0f); + } else { + animation = new ScaleAnimation(0.5f, 1f, 0f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0f); + } + animation.setDuration(120); + view.startAnimation(animation); + animation.setFillAfter(true); + animation.setAnimationListener(listener); + } +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerIndicator.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerIndicator.java new file mode 100644 index 000000000..43f576af6 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerIndicator.java @@ -0,0 +1,100 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.ValueAnimator; + +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; + +public class SimplePagerIndicator extends FrameLayout { + + private ViewPager.OnPageChangeListener mForwardOnPageChangeListener; + + private ValueAnimator mAnimator; + + private int mItemCount; + + private ViewPager.OnPageChangeListener mOnPageChangeListener + = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mAnimator != null) { + int stepSize = 10000 / (mItemCount - 1); + mAnimator.setCurrentPlayTime( + (long) (position * stepSize + positionOffset * stepSize)); + } + mForwardOnPageChangeListener + .onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + mForwardOnPageChangeListener.onPageSelected(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + mForwardOnPageChangeListener.onPageScrollStateChanged(state); + } + }; + + public SimplePagerIndicator(Context context) { + super(context); + } + + public SimplePagerIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setViewPager(final ViewPager viewPager) { + removeAllViews(); + viewPager.addOnPageChangeListener(mOnPageChangeListener); + mItemCount = viewPager.getAdapter().getCount(); + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(this) { + @Override + public void run() { + View tabIndicator = LayoutInflater.from(getContext()) + .inflate(R.layout.simplepagertabs_tab_indicator, + SimplePagerIndicator.this, false); + tabIndicator.getLayoutParams().width + = getLayedOutView().getWidth() / mItemCount; + addView(tabIndicator); + int xGoal = getLayedOutView().getWidth() + - getLayedOutView().getWidth() / mItemCount; + mAnimator = ObjectAnimator.ofFloat(tabIndicator, "x", 0, xGoal); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.setDuration(10000); + } + }); + } + + public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) { + mForwardOnPageChangeListener = onPageChangeListener; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerTabs.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerTabs.java new file mode 100644 index 000000000..798214512 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/SimplePagerTabs.java @@ -0,0 +1,123 @@ +/* == This file is part of Tomahawk Player - === + * + * Copyright 2015, Enno Gottschalk + * + * Tomahawk is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tomahawk is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tomahawk. If not, see . + */ +package org.tomahawk.tomahawk_android.views; + +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.ValueAnimator; + +import org.tomahawk.libtomahawk.utils.ViewUtils; +import org.tomahawk.tomahawk_android.R; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class SimplePagerTabs extends FrameLayout { + + private ViewPager.OnPageChangeListener mForwardOnPageChangeListener; + + private ValueAnimator mAnimator; + + private int mItemCount; + + private ViewPager.OnPageChangeListener mOnPageChangeListener + = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mAnimator != null) { + int stepSize = 10000 / (mItemCount - 1); + mAnimator.setCurrentPlayTime( + (long) (position * stepSize + positionOffset * stepSize)); + } + mForwardOnPageChangeListener + .onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + mForwardOnPageChangeListener.onPageSelected(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + mForwardOnPageChangeListener.onPageScrollStateChanged(state); + } + }; + + public SimplePagerTabs(Context context) { + super(context); + } + + public SimplePagerTabs(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setViewPager(final ViewPager viewPager) { + removeAllViews(); + viewPager.addOnPageChangeListener(mOnPageChangeListener); + LinearLayout itemContainer = new LinearLayout(getContext()); + addView(itemContainer); + mItemCount = viewPager.getAdapter().getCount(); + for (int i = 0; i < viewPager.getAdapter().getCount(); i++) { + if (i > 0) { + View divider = LayoutInflater.from(getContext()) + .inflate(R.layout.simplepagertabs_tab_divider, itemContainer, false); + itemContainer.addView(divider); + } + TextView item = (TextView) LayoutInflater.from(getContext()) + .inflate(R.layout.simplepagertabs_tab_item, itemContainer, false); + item.setText(viewPager.getAdapter().getPageTitle(i)); + itemContainer.addView(item); + if (i == 0) { + ViewUtils.afterViewGlobalLayout(new ViewUtils.ViewRunnable(item) { + @Override + public void run() { + View tabIndicator = LayoutInflater.from(getContext()) + .inflate(R.layout.simplepagertabs_tab_indicator, + SimplePagerTabs.this, false); + tabIndicator.getLayoutParams().width = getLayedOutView().getWidth(); + addView(tabIndicator); + int xGoal = (mItemCount - 1) * (getLayedOutView().getWidth() + 1) - 1; + mAnimator = ObjectAnimator.ofFloat(tabIndicator, "x", 0, xGoal); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.setDuration(10000); + } + }); + } + + final int j = i; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + viewPager.setCurrentItem(j); + } + }); + } + } + + public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) { + mForwardOnPageChangeListener = onPageChangeListener; + } + +} diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/TopCropImageView.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/TopCropImageView.java new file mode 100644 index 000000000..1df506481 --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/TopCropImageView.java @@ -0,0 +1,59 @@ +package org.tomahawk.tomahawk_android.views; + +import android.content.Context; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class TopCropImageView extends ImageView { + + public TopCropImageView(Context context) { + super(context); + setup(); + } + + public TopCropImageView(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public TopCropImageView(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + setup(); + } + + private void setup() { + setScaleType(ScaleType.MATRIX); + } + + @Override + protected boolean setFrame(int frameLeft, int frameTop, int frameRight, int frameBottom) { + if (getDrawable() != null) { + float frameWidth = frameRight - frameLeft; + float frameHeight = frameBottom - frameTop; + + float originalImageWidth = (float) getDrawable().getIntrinsicWidth(); + float originalImageHeight = (float) getDrawable().getIntrinsicHeight(); + + float usedScaleFactor = 1; + + if ((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) { + // If frame is bigger than image + // => Crop it, keep aspect ratio and position it at the bottom and center horizontally + + float fitHorizontallyScaleFactor = frameWidth / originalImageWidth; + float fitVerticallyScaleFactor = frameHeight / originalImageHeight; + + usedScaleFactor = Math.max(fitHorizontallyScaleFactor, fitVerticallyScaleFactor); + } + + Matrix matrix = getImageMatrix(); + matrix.setScale(usedScaleFactor, usedScaleFactor, 0, + 0); // Replaces the old matrix completly + setImageMatrix(matrix); + } + return super.setFrame(frameLeft, frameTop, frameRight, frameBottom); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/tomahawk/tomahawk_android/views/VerticalSeekBar.java b/app/src/main/java/org/tomahawk/tomahawk_android/views/VerticalSeekBar.java new file mode 100644 index 000000000..ac02b013c --- /dev/null +++ b/app/src/main/java/org/tomahawk/tomahawk_android/views/VerticalSeekBar.java @@ -0,0 +1,115 @@ +/***************************************************************************** + * VerticalSeekBar.java + ***************************************************************************** + * Copyright © 2013 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +package org.tomahawk.tomahawk_android.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.SeekBar; + +public class VerticalSeekBar extends SeekBar { + + private boolean mIsMovingThumb = false; + private static final float THUMB_SLOP = 25; + + public VerticalSeekBar(Context context) { + super(context); + } + + public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public VerticalSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(h, w, oldh, oldw); + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(heightMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); + } + + @Override + protected void onDraw(@NonNull Canvas c) { + c.rotate(-90); + c.translate(-getHeight(), 0); + + super.onDraw(c); + } + + @Override + public synchronized void setProgress(int progress) { + super.setProgress(progress); + onSizeChanged(getWidth(), getHeight(), 0, 0); + } + + private boolean isWithinThumb(MotionEvent event) { + final float progress = getProgress(); + final float density = this.getResources().getDisplayMetrics().density; + final float height = getHeight(); + final float y = event.getY(); + final float max = getMax(); + return progress >= max - (int) (max * (y + THUMB_SLOP * density) / height) + && progress <= max - (int) (max * (y - THUMB_SLOP * density) / height); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + if (!isEnabled()) { + return false; + } + + boolean handled = false; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isWithinThumb(event)) { + getParent().requestDisallowInterceptTouchEvent(true); + mIsMovingThumb = true; + handled = true; + } + break; + case MotionEvent.ACTION_MOVE: + if (mIsMovingThumb) { + final int max = getMax(); + setProgress( max - (int) (max* event.getY() / getHeight())); + onSizeChanged(getWidth(), getHeight(), 0, 0); + handled = true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + getParent().requestDisallowInterceptTouchEvent(false); + mIsMovingThumb = false; + handled = true; + break; + } + return handled; + } +} diff --git a/res/drawable-hdpi/ab_bottom_solid_tomahawk.9.png b/app/src/main/res/drawable-hdpi/ab_bottom_solid_tomahawk.9.png similarity index 100% rename from res/drawable-hdpi/ab_bottom_solid_tomahawk.9.png rename to app/src/main/res/drawable-hdpi/ab_bottom_solid_tomahawk.9.png diff --git a/app/src/main/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png b/app/src/main/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png new file mode 100644 index 000000000..fae720aba Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015_disabled.png b/app/src/main/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015_disabled.png new file mode 100644 index 000000000..874edbff6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_default_holo.png b/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_default_holo.png new file mode 100644 index 000000000..ed8c7c442 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_default_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_pressed_holo.png b/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_pressed_holo.png new file mode 100644 index 000000000..de3f083e9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_fastscroll_thumb_pressed_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100644 index 000000000..1ad432b32 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png new file mode 100644 index 000000000..13aec4d00 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png new file mode 100644 index 000000000..207d6d4c1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100644 index 000000000..947b3e3f0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_primary_holo.9.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_primary_holo.9.png new file mode 100644 index 000000000..f46a39ec4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_primary_holo.9.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_secondary_holo.9.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_secondary_holo.9.png new file mode 100644 index 000000000..895ab1f6d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_secondary_holo.9.png differ diff --git a/app/src/main/res/drawable-hdpi/apptheme_scrubber_track_holo_light.9.png b/app/src/main/res/drawable-hdpi/apptheme_scrubber_track_holo_light.9.png new file mode 100644 index 000000000..90528b130 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/apptheme_scrubber_track_holo_light.9.png differ diff --git a/app/src/main/res/drawable-hdpi/artist_placeholder_star.png b/app/src/main/res/drawable-hdpi/artist_placeholder_star.png new file mode 100644 index 000000000..16aad6364 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/artist_placeholder_star.png differ diff --git a/app/src/main/res/drawable-hdpi/collection_header.png b/app/src/main/res/drawable-hdpi/collection_header.png new file mode 100644 index 000000000..f66cb5e82 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/collection_header.png differ diff --git a/app/src/main/res/drawable-hdpi/edittext_background.9.png b/app/src/main/res/drawable-hdpi/edittext_background.9.png new file mode 100644 index 000000000..adb049483 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/edittext_background.9.png differ diff --git a/app/src/main/res/drawable-hdpi/following_button_bg.9.png b/app/src/main/res/drawable-hdpi/following_button_bg.9.png new file mode 100644 index 000000000..c9e1a02d1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/following_button_bg.9.png differ diff --git a/app/src/main/res/drawable-hdpi/following_button_bg_filled.9.png b/app/src/main/res/drawable-hdpi/following_button_bg_filled.9.png new file mode 100644 index 000000000..4e0b6a611 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/following_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-hdpi/gray_button_bg_filled.9.png b/app/src/main/res/drawable-hdpi/gray_button_bg_filled.9.png new file mode 100644 index 000000000..3c5222df3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/gray_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_activity.png b/app/src/main/res/drawable-hdpi/ic_action_activity.png new file mode 100755 index 000000000..812685a11 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_activity.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_charts.png b/app/src/main/res/drawable-hdpi/ic_action_charts.png new file mode 100755 index 000000000..959b3e146 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_charts.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_collection.png b/app/src/main/res/drawable-hdpi/ic_action_collection.png new file mode 100644 index 000000000..1f7c5dfde Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_collection.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_dashboard.png b/app/src/main/res/drawable-hdpi/ic_action_dashboard.png new file mode 100644 index 000000000..6d8efbc8f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_dashboard.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_favorite_large.png b/app/src/main/res/drawable-hdpi/ic_action_favorite_large.png new file mode 100644 index 000000000..112a018a9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_favorite_large.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_favorites.png b/app/src/main/res/drawable-hdpi/ic_action_favorites.png new file mode 100644 index 000000000..56c2fc0bb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_favorites.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_favorites_small.png b/app/src/main/res/drawable-hdpi/ic_action_favorites_small.png new file mode 100644 index 000000000..632af9278 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_favorites_small.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_friend.png b/app/src/main/res/drawable-hdpi/ic_action_friend.png new file mode 100755 index 000000000..066528007 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_friend.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_headphones.png b/app/src/main/res/drawable-hdpi/ic_action_headphones.png new file mode 100755 index 000000000..035a0af73 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_headphones.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_history.png b/app/src/main/res/drawable-hdpi/ic_action_history.png new file mode 100755 index 000000000..27882d623 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_history.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_inbox.png b/app/src/main/res/drawable-hdpi/ic_action_inbox.png new file mode 100755 index 000000000..8ec692d85 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_inbox.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_more.png b/app/src/main/res/drawable-hdpi/ic_action_more.png new file mode 100644 index 000000000..b9480addd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_more.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_newreleases.png b/app/src/main/res/drawable-hdpi/ic_action_newreleases.png new file mode 100755 index 000000000..bd1137189 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_newreleases.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_playlist.png b/app/src/main/res/drawable-hdpi/ic_action_playlist.png new file mode 100644 index 000000000..76c07cfb6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_playlist.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_plugins.png b/app/src/main/res/drawable-hdpi/ic_action_plugins.png new file mode 100755 index 000000000..5a973afe0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_plugins.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_queue.png b/app/src/main/res/drawable-hdpi/ic_action_queue.png new file mode 100755 index 000000000..b25a6e1d8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_queue.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_queue_red.png b/app/src/main/res/drawable-hdpi/ic_action_queue_red.png new file mode 100644 index 000000000..5a0920310 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_queue_red.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_search.png b/app/src/main/res/drawable-hdpi/ic_action_search.png new file mode 100644 index 000000000..bbfbc96cb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_settings.png b/app/src/main/res/drawable-hdpi/ic_action_settings.png new file mode 100644 index 000000000..ea263caad Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_share.png b/app/src/main/res/drawable-hdpi/ic_action_share.png new file mode 100644 index 000000000..5b0650fb7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_share.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_station.png b/app/src/main/res/drawable-hdpi/ic_action_station.png new file mode 100644 index 000000000..5e09389c8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_station.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_trash.png b/app/src/main/res/drawable-hdpi/ic_action_trash.png new file mode 100644 index 000000000..4c1a17ae0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_trash.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_unfavorite_large.png b/app/src/main/res/drawable-hdpi/ic_action_unfavorite_large.png new file mode 100644 index 000000000..5cc32544d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_unfavorite_large.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_alert_error.png b/app/src/main/res/drawable-hdpi/ic_alert_error.png new file mode 100644 index 000000000..69cbb1e0b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_alert_error.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_av_pause.png b/app/src/main/res/drawable-hdpi/ic_av_pause.png new file mode 100644 index 000000000..4d2ea05c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_av_pause.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_av_play_arrow.png b/app/src/main/res/drawable-hdpi/ic_av_play_arrow.png new file mode 100644 index 000000000..57c9fa546 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_av_play_arrow.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_connect.png b/app/src/main/res/drawable-hdpi/ic_connect.png new file mode 100644 index 000000000..610e4d9d7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_connect.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_connected.png b/app/src/main/res/drawable-hdpi/ic_connected.png new file mode 100644 index 000000000..7431a5166 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_connected.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_hardware_smartphone.png b/app/src/main/res/drawable-hdpi/ic_hardware_smartphone.png new file mode 100644 index 000000000..15a581d3d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_hatchet.png b/app/src/main/res/drawable-hdpi/ic_hatchet.png new file mode 100644 index 000000000..f166ad986 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_hatchet.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_hatchet_white.png b/app/src/main/res/drawable-hdpi/ic_hatchet_white.png new file mode 100644 index 000000000..4ab236d1f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_hatchet_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_image_music_note.png b/app/src/main/res/drawable-hdpi/ic_image_music_note.png new file mode 100644 index 000000000..e78e02612 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_image_music_note.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..693b4d123 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_navigation_chevron_left.png b/app/src/main/res/drawable-hdpi/ic_navigation_chevron_left.png new file mode 100644 index 000000000..bd23a0696 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_navigation_chevron_left.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_navigation_chevron_right.png b/app/src/main/res/drawable-hdpi/ic_navigation_chevron_right.png new file mode 100644 index 000000000..1f10ee461 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_navigation_chevron_right.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_navigation_close.png b/app/src/main/res/drawable-hdpi/ic_navigation_close.png new file mode 100644 index 000000000..ceb1a1eeb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_navigation_close.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_navigation_expand_less.png b/app/src/main/res/drawable-hdpi/ic_navigation_expand_less.png new file mode 100644 index 000000000..dea898838 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_navigation_expand_less.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_navigation_expand_more.png b/app/src/main/res/drawable-hdpi/ic_navigation_expand_more.png new file mode 100644 index 000000000..022e05799 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_navigation_expand_more.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 000000000..3584bcce9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_player_next_light.png b/app/src/main/res/drawable-hdpi/ic_player_next_light.png new file mode 100755 index 000000000..98530da1e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_player_next_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_player_previous_light.png b/app/src/main/res/drawable-hdpi/ic_player_previous_light.png new file mode 100755 index 000000000..0b1634daa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_player_previous_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_player_shuffle_light.png b/app/src/main/res/drawable-hdpi/ic_player_shuffle_light.png new file mode 100755 index 000000000..3aa92f53c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_player_shuffle_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_spotify.png b/app/src/main/res/drawable-hdpi/ic_spotify.png new file mode 100644 index 000000000..eb8769f65 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_spotify.png differ diff --git a/res/drawable-hdpi/list_focused_tomahawk.9.png b/app/src/main/res/drawable-hdpi/list_focused_tomahawk.9.png similarity index 100% rename from res/drawable-hdpi/list_focused_tomahawk.9.png rename to app/src/main/res/drawable-hdpi/list_focused_tomahawk.9.png diff --git a/app/src/main/res/drawable-hdpi/list_pressed_tomahawk.9.png b/app/src/main/res/drawable-hdpi/list_pressed_tomahawk.9.png new file mode 100644 index 000000000..5affb72e6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/list_pressed_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-hdpi/playlists_header.png b/app/src/main/res/drawable-hdpi/playlists_header.png new file mode 100644 index 000000000..9fbc834de Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlists_header.png differ diff --git a/app/src/main/res/drawable-hdpi/repeat_all.png b/app/src/main/res/drawable-hdpi/repeat_all.png new file mode 100644 index 000000000..8a8dafa57 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/repeat_all.png differ diff --git a/app/src/main/res/drawable-hdpi/repeat_one.png b/app/src/main/res/drawable-hdpi/repeat_one.png new file mode 100644 index 000000000..30d6532cd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/repeat_one.png differ diff --git a/app/src/main/res/drawable-hdpi/settings_header.png b/app/src/main/res/drawable-hdpi/settings_header.png new file mode 100644 index 000000000..216e125e0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/settings_header.png differ diff --git a/app/src/main/res/drawable-hdpi/splash.png b/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 000000000..7c01af2f7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/app/src/main/res/drawable-hdpi/stations_header.png b/app/src/main/res/drawable-hdpi/stations_header.png new file mode 100644 index 000000000..075869fb5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/stations_header.png differ diff --git a/app/src/main/res/drawable-hdpi/tomahawk_progress_bg_holo_light.9.png b/app/src/main/res/drawable-hdpi/tomahawk_progress_bg_holo_light.9.png new file mode 100644 index 000000000..b1c232f12 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tomahawk_progress_bg_holo_light.9.png differ diff --git a/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_holo_light.9.png b/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_holo_light.9.png new file mode 100644 index 000000000..a908ab2b1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_white.9.png b/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_white.9.png new file mode 100644 index 000000000..e83213bff Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tomahawk_progress_primary_white.9.png differ diff --git a/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_holo_light.9.png b/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_holo_light.9.png new file mode 100644 index 000000000..5c7139b74 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_white.9.png b/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_white.9.png new file mode 100644 index 000000000..479cfed33 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/tomahawk_progress_secondary_white.9.png differ diff --git a/app/src/main/res/drawable-hdpi/white_button_bg.9.png b/app/src/main/res/drawable-hdpi/white_button_bg.9.png new file mode 100644 index 000000000..556c7927a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/white_button_bg.9.png differ diff --git a/res/drawable-mdpi/ab_bottom_solid_tomahawk.9.png b/app/src/main/res/drawable-mdpi/ab_bottom_solid_tomahawk.9.png similarity index 100% rename from res/drawable-mdpi/ab_bottom_solid_tomahawk.9.png rename to app/src/main/res/drawable-mdpi/ab_bottom_solid_tomahawk.9.png diff --git a/app/src/main/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png b/app/src/main/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png new file mode 100644 index 000000000..f72f540e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015_disabled.png b/app/src/main/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015_disabled.png new file mode 100644 index 000000000..8aa1be2b6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_default_holo.png b/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_default_holo.png new file mode 100644 index 000000000..65bb846e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_default_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_pressed_holo.png b/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_pressed_holo.png new file mode 100644 index 000000000..1be2e1ca1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_fastscroll_thumb_pressed_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100644 index 000000000..4aa022a33 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png new file mode 100644 index 000000000..6f6e2d118 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png new file mode 100644 index 000000000..c86e3b346 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100644 index 000000000..72b206988 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_primary_holo.9.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_primary_holo.9.png new file mode 100644 index 000000000..4c864970c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_primary_holo.9.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_secondary_holo.9.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_secondary_holo.9.png new file mode 100644 index 000000000..2e71e0422 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_secondary_holo.9.png differ diff --git a/app/src/main/res/drawable-mdpi/apptheme_scrubber_track_holo_light.9.png b/app/src/main/res/drawable-mdpi/apptheme_scrubber_track_holo_light.9.png new file mode 100644 index 000000000..359ae4a1b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/apptheme_scrubber_track_holo_light.9.png differ diff --git a/app/src/main/res/drawable-mdpi/artist_placeholder_star.png b/app/src/main/res/drawable-mdpi/artist_placeholder_star.png new file mode 100644 index 000000000..42664e606 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/artist_placeholder_star.png differ diff --git a/app/src/main/res/drawable-mdpi/collection_header.png b/app/src/main/res/drawable-mdpi/collection_header.png new file mode 100644 index 000000000..02fb79246 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/collection_header.png differ diff --git a/app/src/main/res/drawable-mdpi/edittext_background.9.png b/app/src/main/res/drawable-mdpi/edittext_background.9.png new file mode 100644 index 000000000..7249c01b5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/edittext_background.9.png differ diff --git a/app/src/main/res/drawable-mdpi/following_button_bg.9.png b/app/src/main/res/drawable-mdpi/following_button_bg.9.png new file mode 100644 index 000000000..3106f7307 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/following_button_bg.9.png differ diff --git a/app/src/main/res/drawable-mdpi/following_button_bg_filled.9.png b/app/src/main/res/drawable-mdpi/following_button_bg_filled.9.png new file mode 100644 index 000000000..e5a309ec9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/following_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-mdpi/gray_button_bg_filled.9.png b/app/src/main/res/drawable-mdpi/gray_button_bg_filled.9.png new file mode 100644 index 000000000..880705c80 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/gray_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_activity.png b/app/src/main/res/drawable-mdpi/ic_action_activity.png new file mode 100755 index 000000000..a0aff2ed9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_activity.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_charts.png b/app/src/main/res/drawable-mdpi/ic_action_charts.png new file mode 100755 index 000000000..63faa2a8f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_charts.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_collection.png b/app/src/main/res/drawable-mdpi/ic_action_collection.png new file mode 100644 index 000000000..b9d735669 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_collection.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_dashboard.png b/app/src/main/res/drawable-mdpi/ic_action_dashboard.png new file mode 100644 index 000000000..de6a80370 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_dashboard.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_favorite_large.png b/app/src/main/res/drawable-mdpi/ic_action_favorite_large.png new file mode 100644 index 000000000..4232719fa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_favorite_large.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_favorites.png b/app/src/main/res/drawable-mdpi/ic_action_favorites.png new file mode 100644 index 000000000..67bc1971c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_favorites.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_favorites_small.png b/app/src/main/res/drawable-mdpi/ic_action_favorites_small.png new file mode 100644 index 000000000..dc734cfe5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_favorites_small.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_friend.png b/app/src/main/res/drawable-mdpi/ic_action_friend.png new file mode 100755 index 000000000..814321d88 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_friend.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_headphones.png b/app/src/main/res/drawable-mdpi/ic_action_headphones.png new file mode 100755 index 000000000..8cae0bdb5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_headphones.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_history.png b/app/src/main/res/drawable-mdpi/ic_action_history.png new file mode 100755 index 000000000..77d071e8b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_history.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_inbox.png b/app/src/main/res/drawable-mdpi/ic_action_inbox.png new file mode 100755 index 000000000..253c56eb3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_inbox.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_more.png b/app/src/main/res/drawable-mdpi/ic_action_more.png new file mode 100644 index 000000000..0b5ea6cc6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_more.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_newreleases.png b/app/src/main/res/drawable-mdpi/ic_action_newreleases.png new file mode 100755 index 000000000..367d8bcdb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_newreleases.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_playlist.png b/app/src/main/res/drawable-mdpi/ic_action_playlist.png new file mode 100644 index 000000000..a3401bf8f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_playlist.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_plugins.png b/app/src/main/res/drawable-mdpi/ic_action_plugins.png new file mode 100755 index 000000000..6958d86cf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_plugins.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_queue.png b/app/src/main/res/drawable-mdpi/ic_action_queue.png new file mode 100755 index 000000000..a5f51ddf0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_queue.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_queue_red.png b/app/src/main/res/drawable-mdpi/ic_action_queue_red.png new file mode 100644 index 000000000..55c03aed4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_queue_red.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_search.png b/app/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 000000000..faefc59c8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_settings.png b/app/src/main/res/drawable-mdpi/ic_action_settings.png new file mode 100644 index 000000000..805220fb0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_settings.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_share.png b/app/src/main/res/drawable-mdpi/ic_action_share.png new file mode 100644 index 000000000..0b5ae5db4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_share.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_station.png b/app/src/main/res/drawable-mdpi/ic_action_station.png new file mode 100644 index 000000000..9c4751c30 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_station.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_trash.png b/app/src/main/res/drawable-mdpi/ic_action_trash.png new file mode 100644 index 000000000..3f01f5070 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_trash.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_unfavorite_large.png b/app/src/main/res/drawable-mdpi/ic_action_unfavorite_large.png new file mode 100644 index 000000000..79da091a3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_unfavorite_large.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_alert_error.png b/app/src/main/res/drawable-mdpi/ic_alert_error.png new file mode 100644 index 000000000..ca148fc7c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_alert_error.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_av_pause.png b/app/src/main/res/drawable-mdpi/ic_av_pause.png new file mode 100644 index 000000000..2272d478c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_av_pause.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_av_play_arrow.png b/app/src/main/res/drawable-mdpi/ic_av_play_arrow.png new file mode 100644 index 000000000..c61e948bb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_av_play_arrow.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_connect.png b/app/src/main/res/drawable-mdpi/ic_connect.png new file mode 100644 index 000000000..d4bef3034 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_connect.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_connected.png b/app/src/main/res/drawable-mdpi/ic_connected.png new file mode 100644 index 000000000..bee5dac4b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_connected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_hardware_smartphone.png b/app/src/main/res/drawable-mdpi/ic_hardware_smartphone.png new file mode 100644 index 000000000..42d23eebd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_hatchet.png b/app/src/main/res/drawable-mdpi/ic_hatchet.png new file mode 100644 index 000000000..0389d7da4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_hatchet.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_hatchet_white.png b/app/src/main/res/drawable-mdpi/ic_hatchet_white.png new file mode 100644 index 000000000..53dcc5b0c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_hatchet_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_image_music_note.png b/app/src/main/res/drawable-mdpi/ic_image_music_note.png new file mode 100644 index 000000000..c3f81e50f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_image_music_note.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..e6cd4d8e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_navigation_chevron_left.png b/app/src/main/res/drawable-mdpi/ic_navigation_chevron_left.png new file mode 100644 index 000000000..4d7869d26 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_navigation_chevron_left.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_navigation_chevron_right.png b/app/src/main/res/drawable-mdpi/ic_navigation_chevron_right.png new file mode 100644 index 000000000..b4f3c6d4c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_navigation_chevron_right.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_navigation_close.png b/app/src/main/res/drawable-mdpi/ic_navigation_close.png new file mode 100644 index 000000000..af7f8288d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_navigation_close.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_navigation_expand_less.png b/app/src/main/res/drawable-mdpi/ic_navigation_expand_less.png new file mode 100644 index 000000000..a2e4baad0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_navigation_expand_less.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_navigation_expand_more.png b/app/src/main/res/drawable-mdpi/ic_navigation_expand_more.png new file mode 100644 index 000000000..910bb2a0a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_navigation_expand_more.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 000000000..9a8850402 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_player_next_light.png b/app/src/main/res/drawable-mdpi/ic_player_next_light.png new file mode 100755 index 000000000..a7e27a626 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_player_next_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_player_previous_light.png b/app/src/main/res/drawable-mdpi/ic_player_previous_light.png new file mode 100755 index 000000000..423d1c823 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_player_previous_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_player_shuffle_light.png b/app/src/main/res/drawable-mdpi/ic_player_shuffle_light.png new file mode 100755 index 000000000..479fdb6a2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_player_shuffle_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_spotify.png b/app/src/main/res/drawable-mdpi/ic_spotify.png new file mode 100644 index 000000000..f01e71bab Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_spotify.png differ diff --git a/res/drawable-mdpi/list_focused_tomahawk.9.png b/app/src/main/res/drawable-mdpi/list_focused_tomahawk.9.png similarity index 100% rename from res/drawable-mdpi/list_focused_tomahawk.9.png rename to app/src/main/res/drawable-mdpi/list_focused_tomahawk.9.png diff --git a/app/src/main/res/drawable-mdpi/list_pressed_tomahawk.9.png b/app/src/main/res/drawable-mdpi/list_pressed_tomahawk.9.png new file mode 100644 index 000000000..a58996d4d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/list_pressed_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-mdpi/playlists_header.png b/app/src/main/res/drawable-mdpi/playlists_header.png new file mode 100644 index 000000000..8cbddcf77 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlists_header.png differ diff --git a/app/src/main/res/drawable-mdpi/repeat_all.png b/app/src/main/res/drawable-mdpi/repeat_all.png new file mode 100644 index 000000000..5df1420c9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/repeat_all.png differ diff --git a/app/src/main/res/drawable-mdpi/repeat_one.png b/app/src/main/res/drawable-mdpi/repeat_one.png new file mode 100644 index 000000000..63b62a7d6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/repeat_one.png differ diff --git a/app/src/main/res/drawable-mdpi/settings_header.png b/app/src/main/res/drawable-mdpi/settings_header.png new file mode 100644 index 000000000..bc1fbb29d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/settings_header.png differ diff --git a/app/src/main/res/drawable-mdpi/splash.png b/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 000000000..5925fd799 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/app/src/main/res/drawable-mdpi/stations_header.png b/app/src/main/res/drawable-mdpi/stations_header.png new file mode 100644 index 000000000..532020548 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/stations_header.png differ diff --git a/app/src/main/res/drawable-mdpi/tomahawk_progress_bg_holo_light.9.png b/app/src/main/res/drawable-mdpi/tomahawk_progress_bg_holo_light.9.png new file mode 100644 index 000000000..24db7aafa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tomahawk_progress_bg_holo_light.9.png differ diff --git a/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_holo_light.9.png b/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_holo_light.9.png new file mode 100644 index 000000000..4943201ff Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_white.9.png b/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_white.9.png new file mode 100644 index 000000000..83042e26f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tomahawk_progress_primary_white.9.png differ diff --git a/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_holo_light.9.png b/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_holo_light.9.png new file mode 100644 index 000000000..462979851 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_white.9.png b/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_white.9.png new file mode 100644 index 000000000..acae0f2b7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/tomahawk_progress_secondary_white.9.png differ diff --git a/app/src/main/res/drawable-mdpi/white_button_bg.9.png b/app/src/main/res/drawable-mdpi/white_button_bg.9.png new file mode 100644 index 000000000..75d8c667e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/white_button_bg.9.png differ diff --git a/app/src/main/res/drawable-sw600dp/collection_header.png b/app/src/main/res/drawable-sw600dp/collection_header.png new file mode 100644 index 000000000..0626d9ea2 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/collection_header.png differ diff --git a/app/src/main/res/drawable-sw600dp/ic_action_favorite_large.png b/app/src/main/res/drawable-sw600dp/ic_action_favorite_large.png new file mode 100644 index 000000000..04c3d9ed9 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/ic_action_favorite_large.png differ diff --git a/app/src/main/res/drawable-sw600dp/ic_action_unfavorite_large.png b/app/src/main/res/drawable-sw600dp/ic_action_unfavorite_large.png new file mode 100644 index 000000000..819b00694 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/ic_action_unfavorite_large.png differ diff --git a/app/src/main/res/drawable-sw600dp/ic_hardware_smartphone.png b/app/src/main/res/drawable-sw600dp/ic_hardware_smartphone.png new file mode 100644 index 000000000..cd672dd14 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-sw600dp/playlists_header.png b/app/src/main/res/drawable-sw600dp/playlists_header.png new file mode 100644 index 000000000..a3609b604 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/playlists_header.png differ diff --git a/app/src/main/res/drawable-sw600dp/settings_header.png b/app/src/main/res/drawable-sw600dp/settings_header.png new file mode 100644 index 000000000..197612eb1 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/settings_header.png differ diff --git a/app/src/main/res/drawable-sw600dp/stations_header.png b/app/src/main/res/drawable-sw600dp/stations_header.png new file mode 100644 index 000000000..b3277e10c Binary files /dev/null and b/app/src/main/res/drawable-sw600dp/stations_header.png differ diff --git a/app/src/main/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png b/app/src/main/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png new file mode 100644 index 000000000..8aca0fec6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png b/app/src/main/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png new file mode 100644 index 000000000..b62ee13c1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015_disabled.png b/app/src/main/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015_disabled.png new file mode 100644 index 000000000..5f40d737d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_default_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_default_holo.png new file mode 100644 index 000000000..4f28a1d6e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_default_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_pressed_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_pressed_holo.png new file mode 100644 index 000000000..0510f24b7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_fastscroll_thumb_pressed_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100644 index 000000000..98f122c19 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png new file mode 100644 index 000000000..84f9d8bea Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png new file mode 100644 index 000000000..88f88e304 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100644 index 000000000..dae8dccd9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_primary_holo.9.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_primary_holo.9.png new file mode 100644 index 000000000..2714fcd65 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_primary_holo.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_secondary_holo.9.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_secondary_holo.9.png new file mode 100644 index 000000000..a73c12c44 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_secondary_holo.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/apptheme_scrubber_track_holo_light.9.png b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_track_holo_light.9.png new file mode 100644 index 000000000..a7d396de2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/apptheme_scrubber_track_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/artist_placeholder_star.png b/app/src/main/res/drawable-xhdpi/artist_placeholder_star.png new file mode 100644 index 000000000..32ad3a1ff Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/artist_placeholder_star.png differ diff --git a/app/src/main/res/drawable-xhdpi/collection_header.png b/app/src/main/res/drawable-xhdpi/collection_header.png new file mode 100644 index 000000000..3715c92fa Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/collection_header.png differ diff --git a/app/src/main/res/drawable-xhdpi/edittext_background.9.png b/app/src/main/res/drawable-xhdpi/edittext_background.9.png new file mode 100644 index 000000000..b2571cb0c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/edittext_background.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/following_button_bg.9.png b/app/src/main/res/drawable-xhdpi/following_button_bg.9.png new file mode 100644 index 000000000..9b746cbea Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/following_button_bg.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/following_button_bg_filled.9.png b/app/src/main/res/drawable-xhdpi/following_button_bg_filled.9.png new file mode 100644 index 000000000..55b78b9a2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/following_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/gray_button_bg_filled.9.png b/app/src/main/res/drawable-xhdpi/gray_button_bg_filled.9.png new file mode 100644 index 000000000..14dad479e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/gray_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_activity.png b/app/src/main/res/drawable-xhdpi/ic_action_activity.png new file mode 100755 index 000000000..db834f476 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_activity.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_charts.png b/app/src/main/res/drawable-xhdpi/ic_action_charts.png new file mode 100755 index 000000000..062fd132f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_charts.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_collection.png b/app/src/main/res/drawable-xhdpi/ic_action_collection.png new file mode 100644 index 000000000..22e1fe6e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_collection.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_dashboard.png b/app/src/main/res/drawable-xhdpi/ic_action_dashboard.png new file mode 100644 index 000000000..2504d530c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_dashboard.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_favorite_large.png b/app/src/main/res/drawable-xhdpi/ic_action_favorite_large.png new file mode 100644 index 000000000..c0bcf4b95 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_favorite_large.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_favorites.png b/app/src/main/res/drawable-xhdpi/ic_action_favorites.png new file mode 100644 index 000000000..2d62b3f75 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_favorites.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_favorites_small.png b/app/src/main/res/drawable-xhdpi/ic_action_favorites_small.png new file mode 100644 index 000000000..d03a5a756 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_favorites_small.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_friend.png b/app/src/main/res/drawable-xhdpi/ic_action_friend.png new file mode 100755 index 000000000..7e7ccc82b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_friend.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_headphones.png b/app/src/main/res/drawable-xhdpi/ic_action_headphones.png new file mode 100755 index 000000000..2147a2284 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_headphones.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_history.png b/app/src/main/res/drawable-xhdpi/ic_action_history.png new file mode 100755 index 000000000..61cd3b64a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_history.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_inbox.png b/app/src/main/res/drawable-xhdpi/ic_action_inbox.png new file mode 100755 index 000000000..4879f6383 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_inbox.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_more.png b/app/src/main/res/drawable-xhdpi/ic_action_more.png new file mode 100644 index 000000000..0c5ed1b9e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_newreleases.png b/app/src/main/res/drawable-xhdpi/ic_action_newreleases.png new file mode 100755 index 000000000..f9d236565 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_newreleases.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playlist.png b/app/src/main/res/drawable-xhdpi/ic_action_playlist.png new file mode 100644 index 000000000..ef277e153 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_playlist.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_plugins.png b/app/src/main/res/drawable-xhdpi/ic_action_plugins.png new file mode 100755 index 000000000..c506dee35 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_plugins.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_queue.png b/app/src/main/res/drawable-xhdpi/ic_action_queue.png new file mode 100755 index 000000000..86550559d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_queue.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_queue_red.png b/app/src/main/res/drawable-xhdpi/ic_action_queue_red.png new file mode 100644 index 000000000..e6b3a7ec9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_queue_red.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_search.png b/app/src/main/res/drawable-xhdpi/ic_action_search.png new file mode 100644 index 000000000..bfc3e3939 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_settings.png b/app/src/main/res/drawable-xhdpi/ic_action_settings.png new file mode 100644 index 000000000..dc8c67104 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_share.png b/app/src/main/res/drawable-xhdpi/ic_action_share.png new file mode 100644 index 000000000..9e3a286e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_share.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_station.png b/app/src/main/res/drawable-xhdpi/ic_action_station.png new file mode 100644 index 000000000..c2a9826cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_station.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_trash.png b/app/src/main/res/drawable-xhdpi/ic_action_trash.png new file mode 100644 index 000000000..3b57187e3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_trash.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_unfavorite_large.png b/app/src/main/res/drawable-xhdpi/ic_action_unfavorite_large.png new file mode 100644 index 000000000..27641c531 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_unfavorite_large.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_alert_error.png b/app/src/main/res/drawable-xhdpi/ic_alert_error.png new file mode 100644 index 000000000..9829698dd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_alert_error.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_av_pause.png b/app/src/main/res/drawable-xhdpi/ic_av_pause.png new file mode 100644 index 000000000..f49aed757 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_av_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_av_play_arrow.png b/app/src/main/res/drawable-xhdpi/ic_av_play_arrow.png new file mode 100644 index 000000000..a3c80e73d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_av_play_arrow.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_connect.png b/app/src/main/res/drawable-xhdpi/ic_connect.png new file mode 100644 index 000000000..c3015db55 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_connect.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_connected.png b/app/src/main/res/drawable-xhdpi/ic_connected.png new file mode 100644 index 000000000..2a188859d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_connected.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_hardware_smartphone.png b/app/src/main/res/drawable-xhdpi/ic_hardware_smartphone.png new file mode 100644 index 000000000..60cadb64e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_hatchet.png b/app/src/main/res/drawable-xhdpi/ic_hatchet.png new file mode 100644 index 000000000..45f7fb82e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_hatchet.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_hatchet_white.png b/app/src/main/res/drawable-xhdpi/ic_hatchet_white.png new file mode 100644 index 000000000..f60765c34 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_hatchet_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_image_music_note.png b/app/src/main/res/drawable-xhdpi/ic_image_music_note.png new file mode 100644 index 000000000..f2773b52b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_image_music_note.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..64cbe0687 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_left.png b/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_left.png new file mode 100644 index 000000000..62f3590ee Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_left.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_right.png b/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_right.png new file mode 100644 index 000000000..93dec392d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_navigation_chevron_right.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_navigation_close.png b/app/src/main/res/drawable-xhdpi/ic_navigation_close.png new file mode 100644 index 000000000..b7c7ffd0e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_navigation_close.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_navigation_expand_less.png b/app/src/main/res/drawable-xhdpi/ic_navigation_expand_less.png new file mode 100644 index 000000000..ae36d91e1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_navigation_expand_less.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_navigation_expand_more.png b/app/src/main/res/drawable-xhdpi/ic_navigation_expand_more.png new file mode 100644 index 000000000..c42e2a049 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_navigation_expand_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 000000000..e04f05286 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_player_next_light.png b/app/src/main/res/drawable-xhdpi/ic_player_next_light.png new file mode 100755 index 000000000..b40c45c92 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_player_next_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_player_previous_light.png b/app/src/main/res/drawable-xhdpi/ic_player_previous_light.png new file mode 100755 index 000000000..a3caf6898 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_player_previous_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_player_shuffle_light.png b/app/src/main/res/drawable-xhdpi/ic_player_shuffle_light.png new file mode 100755 index 000000000..6947cd3af Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_player_shuffle_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_spotify.png b/app/src/main/res/drawable-xhdpi/ic_spotify.png new file mode 100644 index 000000000..837a2c3c3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_spotify.png differ diff --git a/res/drawable-xhdpi/list_focused_tomahawk.9.png b/app/src/main/res/drawable-xhdpi/list_focused_tomahawk.9.png similarity index 100% rename from res/drawable-xhdpi/list_focused_tomahawk.9.png rename to app/src/main/res/drawable-xhdpi/list_focused_tomahawk.9.png diff --git a/app/src/main/res/drawable-xhdpi/list_pressed_tomahawk.9.png b/app/src/main/res/drawable-xhdpi/list_pressed_tomahawk.9.png new file mode 100644 index 000000000..4f6f689b8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/list_pressed_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlists_header.png b/app/src/main/res/drawable-xhdpi/playlists_header.png new file mode 100644 index 000000000..03d49a7ea Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlists_header.png differ diff --git a/app/src/main/res/drawable-xhdpi/repeat_all.png b/app/src/main/res/drawable-xhdpi/repeat_all.png new file mode 100644 index 000000000..f3ab206ae Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/repeat_all.png differ diff --git a/app/src/main/res/drawable-xhdpi/repeat_one.png b/app/src/main/res/drawable-xhdpi/repeat_one.png new file mode 100644 index 000000000..c70fdef3f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/repeat_one.png differ diff --git a/app/src/main/res/drawable-xhdpi/settings_header.png b/app/src/main/res/drawable-xhdpi/settings_header.png new file mode 100644 index 000000000..2651a0b9d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/settings_header.png differ diff --git a/app/src/main/res/drawable-xhdpi/splash.png b/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 000000000..bd21adb37 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/app/src/main/res/drawable-xhdpi/stations_header.png b/app/src/main/res/drawable-xhdpi/stations_header.png new file mode 100644 index 000000000..19e9acb80 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/stations_header.png differ diff --git a/app/src/main/res/drawable-xhdpi/tomahawk_progress_bg_holo_light.9.png b/app/src/main/res/drawable-xhdpi/tomahawk_progress_bg_holo_light.9.png new file mode 100644 index 000000000..00d786a6c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tomahawk_progress_bg_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_holo_light.9.png b/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_holo_light.9.png new file mode 100644 index 000000000..41049cb29 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_white.9.png b/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_white.9.png new file mode 100644 index 000000000..4bd020935 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tomahawk_progress_primary_white.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_holo_light.9.png b/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_holo_light.9.png new file mode 100644 index 000000000..9efffa902 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_white.9.png b/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_white.9.png new file mode 100644 index 000000000..c030ec0d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/tomahawk_progress_secondary_white.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/white_button_bg.9.png b/app/src/main/res/drawable-xhdpi/white_button_bg.9.png new file mode 100644 index 000000000..cb83175ed Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/white_button_bg.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ab_bottom_solid_tomahawk.9.png b/app/src/main/res/drawable-xxhdpi/ab_bottom_solid_tomahawk.9.png new file mode 100644 index 000000000..fcb9578c6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ab_bottom_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ab_stacked_solid_tomahawk.9.png b/app/src/main/res/drawable-xxhdpi/ab_stacked_solid_tomahawk.9.png new file mode 100644 index 000000000..47555f2f7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ab_stacked_solid_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015_disabled.png b/app/src/main/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015_disabled.png new file mode 100644 index 000000000..810a02942 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_default_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_default_holo.png new file mode 100644 index 000000000..9e2b3cab7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_default_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_pressed_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_pressed_holo.png new file mode 100644 index 000000000..34b85c117 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_fastscroll_thumb_pressed_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100644 index 000000000..ee3fbc0a4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png new file mode 100644 index 000000000..53186b5b1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png new file mode 100644 index 000000000..e6f0dbd72 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100644 index 000000000..ff1075571 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_primary_holo.9.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_primary_holo.9.png new file mode 100644 index 000000000..9ab8aba2d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_primary_holo.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_secondary_holo.9.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_secondary_holo.9.png new file mode 100644 index 000000000..3a70c7c6c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_secondary_holo.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_track_holo_light.9.png b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_track_holo_light.9.png new file mode 100644 index 000000000..1a6f577fc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_track_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/artist_placeholder_star.png b/app/src/main/res/drawable-xxhdpi/artist_placeholder_star.png new file mode 100644 index 000000000..a76465c6a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/artist_placeholder_star.png differ diff --git a/app/src/main/res/drawable-xxhdpi/collection_header.png b/app/src/main/res/drawable-xxhdpi/collection_header.png new file mode 100644 index 000000000..efa82b341 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/collection_header.png differ diff --git a/app/src/main/res/drawable-xxhdpi/edittext_background.9.png b/app/src/main/res/drawable-xxhdpi/edittext_background.9.png new file mode 100644 index 000000000..ae89becd2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/edittext_background.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/following_button_bg.9.png b/app/src/main/res/drawable-xxhdpi/following_button_bg.9.png new file mode 100644 index 000000000..c9ca7c48b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/following_button_bg.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/following_button_bg_filled.9.png b/app/src/main/res/drawable-xxhdpi/following_button_bg_filled.9.png new file mode 100644 index 000000000..cdd58e3a1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/following_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/gray_button_bg_filled.9.png b/app/src/main/res/drawable-xxhdpi/gray_button_bg_filled.9.png new file mode 100644 index 000000000..7e435baa5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/gray_button_bg_filled.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_activity.png b/app/src/main/res/drawable-xxhdpi/ic_action_activity.png new file mode 100755 index 000000000..f17f6f8d2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_activity.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_charts.png b/app/src/main/res/drawable-xxhdpi/ic_action_charts.png new file mode 100755 index 000000000..ec3fe2959 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_charts.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_collection.png b/app/src/main/res/drawable-xxhdpi/ic_action_collection.png new file mode 100644 index 000000000..279af4daf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_collection.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_dashboard.png b/app/src/main/res/drawable-xxhdpi/ic_action_dashboard.png new file mode 100644 index 000000000..5c4e84996 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_dashboard.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_favorite_large.png b/app/src/main/res/drawable-xxhdpi/ic_action_favorite_large.png new file mode 100644 index 000000000..04c3d9ed9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_favorite_large.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_favorites.png b/app/src/main/res/drawable-xxhdpi/ic_action_favorites.png new file mode 100644 index 000000000..89ae940c6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_favorites.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_favorites_small.png b/app/src/main/res/drawable-xxhdpi/ic_action_favorites_small.png new file mode 100644 index 000000000..82131d5cc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_favorites_small.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_friend.png b/app/src/main/res/drawable-xxhdpi/ic_action_friend.png new file mode 100755 index 000000000..568774c7f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_friend.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_headphones.png b/app/src/main/res/drawable-xxhdpi/ic_action_headphones.png new file mode 100755 index 000000000..a28b95915 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_headphones.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_history.png b/app/src/main/res/drawable-xxhdpi/ic_action_history.png new file mode 100755 index 000000000..9d0e9f361 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_history.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_inbox.png b/app/src/main/res/drawable-xxhdpi/ic_action_inbox.png new file mode 100755 index 000000000..2fcd1ba6c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_inbox.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_more.png b/app/src/main/res/drawable-xxhdpi/ic_action_more.png new file mode 100644 index 000000000..f719885e3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_more.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_newreleases.png b/app/src/main/res/drawable-xxhdpi/ic_action_newreleases.png new file mode 100755 index 000000000..4d8b95b9e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_newreleases.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playlist.png b/app/src/main/res/drawable-xxhdpi/ic_action_playlist.png new file mode 100644 index 000000000..d0e8b1552 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_playlist.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_plugins.png b/app/src/main/res/drawable-xxhdpi/ic_action_plugins.png new file mode 100755 index 000000000..00eba57f5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_plugins.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_queue.png b/app/src/main/res/drawable-xxhdpi/ic_action_queue.png new file mode 100755 index 000000000..9c2b11ec7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_queue.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_queue_red.png b/app/src/main/res/drawable-xxhdpi/ic_action_queue_red.png new file mode 100644 index 000000000..007751a69 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_queue_red.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_search.png b/app/src/main/res/drawable-xxhdpi/ic_action_search.png new file mode 100644 index 000000000..abbb98951 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_settings.png b/app/src/main/res/drawable-xxhdpi/ic_action_settings.png new file mode 100644 index 000000000..50b634274 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_share.png b/app/src/main/res/drawable-xxhdpi/ic_action_share.png new file mode 100644 index 000000000..0bf864df5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_share.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_station.png b/app/src/main/res/drawable-xxhdpi/ic_action_station.png new file mode 100644 index 000000000..f433b8866 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_station.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_trash.png b/app/src/main/res/drawable-xxhdpi/ic_action_trash.png new file mode 100644 index 000000000..6d117e6a8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_trash.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_unfavorite_large.png b/app/src/main/res/drawable-xxhdpi/ic_action_unfavorite_large.png new file mode 100644 index 000000000..819b00694 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_unfavorite_large.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_alert_error.png b/app/src/main/res/drawable-xxhdpi/ic_alert_error.png new file mode 100644 index 000000000..abe2573b1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alert_error.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_av_pause.png b/app/src/main/res/drawable-xxhdpi/ic_av_pause.png new file mode 100644 index 000000000..7192ad487 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_av_pause.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow.png b/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow.png new file mode 100644 index 000000000..547ef30aa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_connect.png b/app/src/main/res/drawable-xxhdpi/ic_connect.png new file mode 100644 index 000000000..428586c0c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_connect.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_connected.png b/app/src/main/res/drawable-xxhdpi/ic_connected.png new file mode 100644 index 000000000..6207ba218 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_connected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hardware_smartphone.png b/app/src/main/res/drawable-xxhdpi/ic_hardware_smartphone.png new file mode 100644 index 000000000..cd672dd14 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hatchet.png b/app/src/main/res/drawable-xxhdpi/ic_hatchet.png new file mode 100644 index 000000000..b93676f2e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_hatchet.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hatchet_white.png b/app/src/main/res/drawable-xxhdpi/ic_hatchet_white.png new file mode 100644 index 000000000..7ccf9e187 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_hatchet_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_image_music_note.png b/app/src/main/res/drawable-xxhdpi/ic_image_music_note.png new file mode 100644 index 000000000..a9058f3e8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_image_music_note.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..04ca1af37 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_left.png b/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_left.png new file mode 100644 index 000000000..7141cc618 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_left.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_right.png b/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_right.png new file mode 100644 index 000000000..7920aa3d2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_navigation_chevron_right.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_navigation_close.png b/app/src/main/res/drawable-xxhdpi/ic_navigation_close.png new file mode 100644 index 000000000..6b717e0dd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_navigation_close.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_less.png b/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_less.png new file mode 100644 index 000000000..62fc386c1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_less.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_more.png b/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_more.png new file mode 100644 index 000000000..dbc0b2032 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_navigation_expand_more.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 000000000..31ff3fd87 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_player_next_light.png b/app/src/main/res/drawable-xxhdpi/ic_player_next_light.png new file mode 100755 index 000000000..bfc8fbf98 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_player_next_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_player_previous_light.png b/app/src/main/res/drawable-xxhdpi/ic_player_previous_light.png new file mode 100755 index 000000000..979253a44 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_player_previous_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_player_shuffle_light.png b/app/src/main/res/drawable-xxhdpi/ic_player_shuffle_light.png new file mode 100755 index 000000000..aeeda9d40 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_player_shuffle_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_spotify.png b/app/src/main/res/drawable-xxhdpi/ic_spotify.png new file mode 100644 index 000000000..27a0f4d12 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_spotify.png differ diff --git a/app/src/main/res/drawable-xxhdpi/list_focused_tomahawk.9.png b/app/src/main/res/drawable-xxhdpi/list_focused_tomahawk.9.png new file mode 100644 index 000000000..7320a81fa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/list_focused_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/list_pressed_tomahawk.9.png b/app/src/main/res/drawable-xxhdpi/list_pressed_tomahawk.9.png new file mode 100644 index 000000000..3af5169d3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/list_pressed_tomahawk.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlists_header.png b/app/src/main/res/drawable-xxhdpi/playlists_header.png new file mode 100644 index 000000000..a3609b604 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlists_header.png differ diff --git a/app/src/main/res/drawable-xxhdpi/repeat_all.png b/app/src/main/res/drawable-xxhdpi/repeat_all.png new file mode 100644 index 000000000..e97d9ca37 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/repeat_all.png differ diff --git a/app/src/main/res/drawable-xxhdpi/repeat_one.png b/app/src/main/res/drawable-xxhdpi/repeat_one.png new file mode 100644 index 000000000..a59eed451 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/repeat_one.png differ diff --git a/app/src/main/res/drawable-xxhdpi/settings_header.png b/app/src/main/res/drawable-xxhdpi/settings_header.png new file mode 100644 index 000000000..2c4323c9e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/settings_header.png differ diff --git a/app/src/main/res/drawable-xxhdpi/splash.png b/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 000000000..7a6f39c24 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/app/src/main/res/drawable-xxhdpi/stations_header.png b/app/src/main/res/drawable-xxhdpi/stations_header.png new file mode 100644 index 000000000..b3277e10c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/stations_header.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tomahawk_progress_bg_holo_light.9.png b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_bg_holo_light.9.png new file mode 100644 index 000000000..2f0d8f138 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_bg_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_holo_light.9.png b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_holo_light.9.png new file mode 100644 index 000000000..fcbab9787 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_white.9.png b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_white.9.png new file mode 100644 index 000000000..2daf480d8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_primary_white.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_holo_light.9.png b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_holo_light.9.png new file mode 100644 index 000000000..b98b33db6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_holo_light.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_white.9.png b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_white.9.png new file mode 100644 index 000000000..a5b43a59a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/tomahawk_progress_secondary_white.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/white_button_bg.9.png b/app/src/main/res/drawable-xxhdpi/white_button_bg.9.png new file mode 100644 index 000000000..8c7e5593c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/white_button_bg.9.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_collection.png b/app/src/main/res/drawable-xxxhdpi/ic_action_collection.png new file mode 100644 index 000000000..190a549dc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_collection.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_dashboard.png b/app/src/main/res/drawable-xxxhdpi/ic_action_dashboard.png new file mode 100644 index 000000000..b93ed3e8e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_dashboard.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_favorites.png b/app/src/main/res/drawable-xxxhdpi/ic_action_favorites.png new file mode 100644 index 000000000..3bdd182f9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_favorites.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_favorites_small.png b/app/src/main/res/drawable-xxxhdpi/ic_action_favorites_small.png new file mode 100644 index 000000000..03fa0efe1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_favorites_small.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playlist.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playlist.png new file mode 100644 index 000000000..3133bca65 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_playlist.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_search.png b/app/src/main/res/drawable-xxxhdpi/ic_action_search.png new file mode 100644 index 000000000..dd5adfc7f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_settings.png b/app/src/main/res/drawable-xxxhdpi/ic_action_settings.png new file mode 100644 index 000000000..84d12ec5c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_settings.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_station.png b/app/src/main/res/drawable-xxxhdpi/ic_action_station.png new file mode 100644 index 000000000..db3486d75 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_station.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_alert_error.png b/app/src/main/res/drawable-xxxhdpi/ic_alert_error.png new file mode 100644 index 000000000..830fb7e1e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_alert_error.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_av_pause.png b/app/src/main/res/drawable-xxxhdpi/ic_av_pause.png new file mode 100644 index 000000000..660ac6585 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_av_pause.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow.png b/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow.png new file mode 100644 index 000000000..be5c062b5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_hardware_smartphone.png b/app/src/main/res/drawable-xxxhdpi/ic_hardware_smartphone.png new file mode 100644 index 000000000..e24251183 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_hardware_smartphone.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_image_music_note.png b/app/src/main/res/drawable-xxxhdpi/ic_image_music_note.png new file mode 100644 index 000000000..070ad3599 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_image_music_note.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1ea5e2f6a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_left.png b/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_left.png new file mode 100644 index 000000000..a68bc5fb5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_left.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_right.png b/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_right.png new file mode 100644 index 000000000..6858f02b1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_navigation_chevron_right.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_navigation_close.png b/app/src/main/res/drawable-xxxhdpi/ic_navigation_close.png new file mode 100644 index 000000000..396419219 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_navigation_close.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_less.png b/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_less.png new file mode 100644 index 000000000..42615516b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_less.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_more.png b/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_more.png new file mode 100644 index 000000000..2859a6fec Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_navigation_expand_more.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_player_next_light.png b/app/src/main/res/drawable-xxxhdpi/ic_player_next_light.png new file mode 100644 index 000000000..757b51f1c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_player_next_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_player_previous_light.png b/app/src/main/res/drawable-xxxhdpi/ic_player_previous_light.png new file mode 100644 index 000000000..b018d8489 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_player_previous_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/repeat_all.png b/app/src/main/res/drawable-xxxhdpi/repeat_all.png new file mode 100644 index 000000000..70996952e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/repeat_all.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/repeat_one.png b/app/src/main/res/drawable-xxxhdpi/repeat_one.png new file mode 100644 index 000000000..914db9ed9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/repeat_one.png differ diff --git a/app/src/main/res/drawable/above_shadow.xml b/app/src/main/res/drawable/above_shadow.xml new file mode 100644 index 000000000..cf890b9b6 --- /dev/null +++ b/app/src/main/res/drawable/above_shadow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/above_shadow_darker.xml b/app/src/main/res/drawable/above_shadow_darker.xml new file mode 100644 index 000000000..ec758d465 --- /dev/null +++ b/app/src/main/res/drawable/above_shadow_darker.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/above_shadow_page_indicator.xml b/app/src/main/res/drawable/above_shadow_page_indicator.xml new file mode 100644 index 000000000..b255cc02c --- /dev/null +++ b/app/src/main/res/drawable/above_shadow_page_indicator.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/above_shadow_slightly_darker.xml b/app/src/main/res/drawable/above_shadow_slightly_darker.xml new file mode 100644 index 000000000..8bd4508fd --- /dev/null +++ b/app/src/main/res/drawable/above_shadow_slightly_darker.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/album_placeholder.xml b/app/src/main/res/drawable/album_placeholder.xml new file mode 100644 index 000000000..8ec2677d8 --- /dev/null +++ b/app/src/main/res/drawable/album_placeholder.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/apptheme_fastscroll_thumb_holo.xml b/app/src/main/res/drawable/apptheme_fastscroll_thumb_holo.xml new file mode 100644 index 000000000..e7121f083 --- /dev/null +++ b/app/src/main/res/drawable/apptheme_fastscroll_thumb_holo.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml b/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml new file mode 100644 index 000000000..4eb5c24f1 --- /dev/null +++ b/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml b/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml new file mode 100644 index 000000000..9535874f0 --- /dev/null +++ b/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/artist_placeholder.xml b/app/src/main/res/drawable/artist_placeholder.xml new file mode 100644 index 000000000..89cd7f75e --- /dev/null +++ b/app/src/main/res/drawable/artist_placeholder.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/below_shadow.xml b/app/src/main/res/drawable/below_shadow.xml new file mode 100644 index 000000000..9f0751658 --- /dev/null +++ b/app/src/main/res/drawable/below_shadow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/below_shadow_darker.xml b/app/src/main/res/drawable/below_shadow_darker.xml new file mode 100644 index 000000000..c56e2de2d --- /dev/null +++ b/app/src/main/res/drawable/below_shadow_darker.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_black.xml b/app/src/main/res/drawable/circle_black.xml new file mode 100644 index 000000000..ad5286a32 --- /dev/null +++ b/app/src/main/res/drawable/circle_black.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_blacktransparent.xml b/app/src/main/res/drawable/circle_blacktransparent.xml new file mode 100644 index 000000000..0943af82f --- /dev/null +++ b/app/src/main/res/drawable/circle_blacktransparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_focused.xml b/app/src/main/res/drawable/circle_focused.xml new file mode 100644 index 000000000..35dffab81 --- /dev/null +++ b/app/src/main/res/drawable/circle_focused.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_pressed.xml b/app/src/main/res/drawable/circle_pressed.xml new file mode 100644 index 000000000..a589da17f --- /dev/null +++ b/app/src/main/res/drawable/circle_pressed.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_collection_underlined.xml b/app/src/main/res/drawable/ic_action_collection_underlined.xml new file mode 100644 index 000000000..bc00d6f50 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_collection_underlined.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_favorites_small_underlined.xml b/app/src/main/res/drawable/ic_action_favorites_small_underlined.xml new file mode 100644 index 000000000..30ad86ab3 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_favorites_small_underlined.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_favorites_underlined.xml b/app/src/main/res/drawable/ic_action_favorites_underlined.xml new file mode 100644 index 000000000..c8b931f71 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_favorites_underlined.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/pressed_background_playback_small_tomahawk.xml b/app/src/main/res/drawable/pressed_background_playback_small_tomahawk.xml new file mode 100644 index 000000000..df90ddab4 --- /dev/null +++ b/app/src/main/res/drawable/pressed_background_playback_small_tomahawk.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_black.xml b/app/src/main/res/drawable/rectangle_black.xml new file mode 100644 index 000000000..b7e9a42ce --- /dev/null +++ b/app/src/main/res/drawable/rectangle_black.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_gray.xml b/app/src/main/res/drawable/rectangle_gray.xml new file mode 100644 index 000000000..c928ddd0b --- /dev/null +++ b/app/src/main/res/drawable/rectangle_gray.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_gray_filled.xml b/app/src/main/res/drawable/rectangle_gray_filled.xml new file mode 100644 index 000000000..12d6db331 --- /dev/null +++ b/app/src/main/res/drawable/rectangle_gray_filled.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_red.xml b/app/src/main/res/drawable/rectangle_red.xml new file mode 100644 index 000000000..603a9b2ce --- /dev/null +++ b/app/src/main/res/drawable/rectangle_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ring_red.xml b/app/src/main/res/drawable/ring_red.xml new file mode 100644 index 000000000..fa6fcf2fd --- /dev/null +++ b/app/src/main/res/drawable/ring_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ring_red_filled.xml b/app/src/main/res/drawable/ring_red_filled.xml new file mode 100644 index 000000000..7c760460c --- /dev/null +++ b/app/src/main/res/drawable/ring_red_filled.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_button_green.xml b/app/src/main/res/drawable/selectable_background_button_green.xml new file mode 100644 index 000000000..44e446044 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_button_green.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_button_green_filled.xml b/app/src/main/res/drawable/selectable_background_button_green_filled.xml new file mode 100644 index 000000000..2135128d4 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_button_green_filled.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_button_ring_red.xml b/app/src/main/res/drawable/selectable_background_button_ring_red.xml new file mode 100644 index 000000000..152f90819 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_button_ring_red.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_button_white.xml b/app/src/main/res/drawable/selectable_background_button_white.xml new file mode 100644 index 000000000..ed211f094 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_button_white.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_button_white_transition.xml b/app/src/main/res/drawable/selectable_background_button_white_transition.xml new file mode 100644 index 000000000..733efc1c8 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_button_white_transition.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_deezer_button.xml b/app/src/main/res/drawable/selectable_background_deezer_button.xml new file mode 100644 index 000000000..da1514104 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_deezer_button.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_ring_tomahawk.xml b/app/src/main/res/drawable/selectable_background_ring_tomahawk.xml new file mode 100644 index 000000000..1953f5123 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_ring_tomahawk.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/res/drawable/selectable_background_tomahawk.xml b/app/src/main/res/drawable/selectable_background_tomahawk.xml similarity index 93% rename from res/drawable/selectable_background_tomahawk.xml rename to app/src/main/res/drawable/selectable_background_tomahawk.xml index af6de157d..8c5cfc499 100644 --- a/res/drawable/selectable_background_tomahawk.xml +++ b/app/src/main/res/drawable/selectable_background_tomahawk.xml @@ -1,6 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent.xml b/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent.xml new file mode 100644 index 000000000..5cecb1770 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent_inverted.xml b/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent_inverted.xml new file mode 100644 index 000000000..c97784d8c --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_halftransparent_inverted.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_hatchet_tab.xml b/app/src/main/res/drawable/selectable_background_tomahawk_hatchet_tab.xml new file mode 100644 index 000000000..e7385d17d --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_hatchet_tab.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_opaque.xml b/app/src/main/res/drawable/selectable_background_tomahawk_opaque.xml new file mode 100644 index 000000000..7d4f0b12c --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_opaque.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_opaque_inverted.xml b/app/src/main/res/drawable/selectable_background_tomahawk_opaque_inverted.xml new file mode 100644 index 000000000..0c8aa89bb --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_opaque_inverted.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_rectangle_gray.xml b/app/src/main/res/drawable/selectable_background_tomahawk_rectangle_gray.xml new file mode 100644 index 000000000..7f52d8f4d --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_rectangle_gray.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_red.xml b/app/src/main/res/drawable/selectable_background_tomahawk_red.xml new file mode 100644 index 000000000..44a6051d5 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_red.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_background_tomahawk_red_filled.xml b/app/src/main/res/drawable/selectable_background_tomahawk_red_filled.xml new file mode 100644 index 000000000..e0b099936 --- /dev/null +++ b/app/src/main/res/drawable/selectable_background_tomahawk_red_filled.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selectable_circle.xml b/app/src/main/res/drawable/selectable_circle.xml new file mode 100644 index 000000000..f2678e76f --- /dev/null +++ b/app/src/main/res/drawable/selectable_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_centered.xml b/app/src/main/res/drawable/splash_centered.xml new file mode 100644 index 000000000..f8f88f541 --- /dev/null +++ b/app/src/main/res/drawable/splash_centered.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tomahawk_progress_horizontal_holo_light.xml b/app/src/main/res/drawable/tomahawk_progress_horizontal_holo_light.xml new file mode 100644 index 000000000..0857e0865 --- /dev/null +++ b/app/src/main/res/drawable/tomahawk_progress_horizontal_holo_light.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tomahawk_progress_horizontal_white.xml b/app/src/main/res/drawable/tomahawk_progress_horizontal_white.xml new file mode 100644 index 000000000..aa6ba11b7 --- /dev/null +++ b/app/src/main/res/drawable/tomahawk_progress_horizontal_white.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/triangle_blacktransparent.xml b/app/src/main/res/drawable/triangle_blacktransparent.xml new file mode 100644 index 000000000..7da02b07f --- /dev/null +++ b/app/src/main/res/drawable/triangle_blacktransparent.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/triangle_white.xml b/app/src/main/res/drawable/triangle_white.xml new file mode 100644 index 000000000..eb9d14d0a --- /dev/null +++ b/app/src/main/res/drawable/triangle_white.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/white_underline.xml b/app/src/main/res/drawable/white_underline.xml new file mode 100644 index 000000000..4122bf504 --- /dev/null +++ b/app/src/main/res/drawable/white_underline.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/white_underline_thin.xml b/app/src/main/res/drawable/white_underline_thin.xml new file mode 100644 index 000000000..9591bd047 --- /dev/null +++ b/app/src/main/res/drawable/white_underline_thin.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/album_art_view_pager_item.xml b/app/src/main/res/layout-land/album_art_view_pager_item.xml new file mode 100644 index 000000000..2e78e2550 --- /dev/null +++ b/app/src/main/res/layout-land/album_art_view_pager_item.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/context_menu_button_viewalbum.xml b/app/src/main/res/layout-land/context_menu_button_viewalbum.xml new file mode 100644 index 000000000..5e8790081 --- /dev/null +++ b/app/src/main/res/layout-land/context_menu_button_viewalbum.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/context_menu_item_grid_playback.xml b/app/src/main/res/layout-land/context_menu_item_grid_playback.xml new file mode 100644 index 000000000..e4354c417 --- /dev/null +++ b/app/src/main/res/layout-land/context_menu_item_grid_playback.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/equalizerfragment_layout.xml b/app/src/main/res/layout-land/equalizerfragment_layout.xml new file mode 100644 index 000000000..d41ae7ff7 --- /dev/null +++ b/app/src/main/res/layout-land/equalizerfragment_layout.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/album_art_view_pager_item.xml b/app/src/main/res/layout/album_art_view_pager_item.xml new file mode 100644 index 000000000..dcf2b44c6 --- /dev/null +++ b/app/src/main/res/layout/album_art_view_pager_item.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_ask_access.xml b/app/src/main/res/layout/config_ask_access.xml new file mode 100644 index 000000000..015e99f4c --- /dev/null +++ b/app/src/main/res/layout/config_ask_access.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_button.xml b/app/src/main/res/layout/config_button.xml new file mode 100644 index 000000000..1075c7fb2 --- /dev/null +++ b/app/src/main/res/layout/config_button.xml @@ -0,0 +1,41 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_checkbox.xml b/app/src/main/res/layout/config_checkbox.xml new file mode 100644 index 000000000..36b892972 --- /dev/null +++ b/app/src/main/res/layout/config_checkbox.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_create_station.xml b/app/src/main/res/layout/config_create_station.xml new file mode 100644 index 000000000..21db9ddb1 --- /dev/null +++ b/app/src/main/res/layout/config_create_station.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_dialog.xml b/app/src/main/res/layout/config_dialog.xml new file mode 100644 index 000000000..1d79f55de --- /dev/null +++ b/app/src/main/res/layout/config_dialog.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_directorychooser.xml b/app/src/main/res/layout/config_directorychooser.xml new file mode 100644 index 000000000..92672e8e8 --- /dev/null +++ b/app/src/main/res/layout/config_directorychooser.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_dropdown.xml b/app/src/main/res/layout/config_dropdown.xml new file mode 100644 index 000000000..2cc65bec2 --- /dev/null +++ b/app/src/main/res/layout/config_dropdown.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_edittext.xml b/app/src/main/res/layout/config_edittext.xml new file mode 100644 index 000000000..a5d4db125 --- /dev/null +++ b/app/src/main/res/layout/config_edittext.xml @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_edittext_multiplelines.xml b/app/src/main/res/layout/config_edittext_multiplelines.xml new file mode 100644 index 000000000..cb8d20533 --- /dev/null +++ b/app/src/main/res/layout/config_edittext_multiplelines.xml @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_enable_button.xml b/app/src/main/res/layout/config_enable_button.xml new file mode 100644 index 000000000..25eba62bd --- /dev/null +++ b/app/src/main/res/layout/config_enable_button.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_hatchetloginregister.xml b/app/src/main/res/layout/config_hatchetloginregister.xml new file mode 100644 index 000000000..251933a0c --- /dev/null +++ b/app/src/main/res/layout/config_hatchetloginregister.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_install_plugin.xml b/app/src/main/res/layout/config_install_plugin.xml new file mode 100644 index 000000000..df2f60423 --- /dev/null +++ b/app/src/main/res/layout/config_install_plugin.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_radiobutton.xml b/app/src/main/res/layout/config_radiobutton.xml new file mode 100644 index 000000000..771b7af4f --- /dev/null +++ b/app/src/main/res/layout/config_radiobutton.xml @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_radiogroup.xml b/app/src/main/res/layout/config_radiogroup.xml new file mode 100644 index 000000000..4f2780b29 --- /dev/null +++ b/app/src/main/res/layout/config_radiogroup.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_redirect_button.xml b/app/src/main/res/layout/config_redirect_button.xml new file mode 100644 index 000000000..9ced6df1c --- /dev/null +++ b/app/src/main/res/layout/config_redirect_button.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/config_textview.xml b/app/src/main/res/layout/config_textview.xml new file mode 100644 index 000000000..05af808b7 --- /dev/null +++ b/app/src/main/res/layout/config_textview.xml @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_footer_spacer.xml b/app/src/main/res/layout/content_footer_spacer.xml new file mode 100644 index 000000000..492b565d1 --- /dev/null +++ b/app/src/main/res/layout/content_footer_spacer.xml @@ -0,0 +1,25 @@ + + + diff --git a/app/src/main/res/layout/content_header.xml b/app/src/main/res/layout/content_header.xml new file mode 100644 index 000000000..f4f38fe06 --- /dev/null +++ b/app/src/main/res/layout/content_header.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_header_spacer.xml b/app/src/main/res/layout/content_header_spacer.xml new file mode 100644 index 000000000..ddbcc8e3c --- /dev/null +++ b/app/src/main/res/layout/content_header_spacer.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_header_static.xml b/app/src/main/res/layout/content_header_static.xml new file mode 100644 index 000000000..391fb000f --- /dev/null +++ b/app/src/main/res/layout/content_header_static.xml @@ -0,0 +1,35 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_header_user.xml b/app/src/main/res/layout/content_header_user.xml new file mode 100644 index 000000000..843438231 --- /dev/null +++ b/app/src/main/res/layout/content_header_user.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_header_user_navdrawer.xml b/app/src/main/res/layout/content_header_user_navdrawer.xml new file mode 100644 index 000000000..558d766d1 --- /dev/null +++ b/app/src/main/res/layout/content_header_user_navdrawer.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_albumart.xml b/app/src/main/res/layout/context_menu_albumart.xml new file mode 100644 index 000000000..87cf1eb5a --- /dev/null +++ b/app/src/main/res/layout/context_menu_albumart.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_button_viewalbum.xml b/app/src/main/res/layout/context_menu_button_viewalbum.xml new file mode 100644 index 000000000..d8946d15f --- /dev/null +++ b/app/src/main/res/layout/context_menu_button_viewalbum.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_button_white.xml b/app/src/main/res/layout/context_menu_button_white.xml new file mode 100644 index 000000000..9c125dc06 --- /dev/null +++ b/app/src/main/res/layout/context_menu_button_white.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_button_white_bold.xml b/app/src/main/res/layout/context_menu_button_white_bold.xml new file mode 100644 index 000000000..d758956c6 --- /dev/null +++ b/app/src/main/res/layout/context_menu_button_white_bold.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_button_white_micro.xml b/app/src/main/res/layout/context_menu_button_white_micro.xml new file mode 100644 index 000000000..cc2e080f2 --- /dev/null +++ b/app/src/main/res/layout/context_menu_button_white_micro.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_fragment.xml b/app/src/main/res/layout/context_menu_fragment.xml new file mode 100644 index 000000000..bbc3519b1 --- /dev/null +++ b/app/src/main/res/layout/context_menu_fragment.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_fragment_playback.xml b/app/src/main/res/layout/context_menu_fragment_playback.xml new file mode 100644 index 000000000..fbcb14c29 --- /dev/null +++ b/app/src/main/res/layout/context_menu_fragment_playback.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_item.xml b/app/src/main/res/layout/context_menu_item.xml new file mode 100644 index 000000000..53d3c115c --- /dev/null +++ b/app/src/main/res/layout/context_menu_item.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_item_grid_playback.xml b/app/src/main/res/layout/context_menu_item_grid_playback.xml new file mode 100644 index 000000000..6f8559572 --- /dev/null +++ b/app/src/main/res/layout/context_menu_item_grid_playback.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_textview.xml b/app/src/main/res/layout/context_menu_textview.xml new file mode 100644 index 000000000..eb12d0955 --- /dev/null +++ b/app/src/main/res/layout/context_menu_textview.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/directory_chooser.xml b/app/src/main/res/layout/directory_chooser.xml new file mode 100644 index 000000000..8578af9a6 --- /dev/null +++ b/app/src/main/res/layout/directory_chooser.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dropdown_header.xml b/app/src/main/res/layout/dropdown_header.xml new file mode 100644 index 000000000..a9601d36a --- /dev/null +++ b/app/src/main/res/layout/dropdown_header.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dropdown_header_dropdown_textview.xml b/app/src/main/res/layout/dropdown_header_dropdown_textview.xml new file mode 100644 index 000000000..75c6fca0e --- /dev/null +++ b/app/src/main/res/layout/dropdown_header_dropdown_textview.xml @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dropdown_header_textview.xml b/app/src/main/res/layout/dropdown_header_textview.xml new file mode 100644 index 000000000..f8abc7483 --- /dev/null +++ b/app/src/main/res/layout/dropdown_header_textview.xml @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/equalizerbar.xml b/app/src/main/res/layout/equalizerbar.xml new file mode 100644 index 000000000..1a5564072 --- /dev/null +++ b/app/src/main/res/layout/equalizerbar.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/equalizerfragment_layout.xml b/app/src/main/res/layout/equalizerfragment_layout.xml new file mode 100644 index 000000000..771d62f15 --- /dev/null +++ b/app/src/main/res/layout/equalizerfragment_layout.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fake_preferences_checkbox.xml b/app/src/main/res/layout/fake_preferences_checkbox.xml new file mode 100644 index 000000000..a36f8fc92 --- /dev/null +++ b/app/src/main/res/layout/fake_preferences_checkbox.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fake_preferences_plain.xml b/app/src/main/res/layout/fake_preferences_plain.xml new file mode 100644 index 000000000..21047e912 --- /dev/null +++ b/app/src/main/res/layout/fake_preferences_plain.xml @@ -0,0 +1,50 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fake_preferences_spinner.xml b/app/src/main/res/layout/fake_preferences_spinner.xml new file mode 100644 index 000000000..2207db079 --- /dev/null +++ b/app/src/main/res/layout/fake_preferences_spinner.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fancydropdown.xml b/app/src/main/res/layout/fancydropdown.xml new file mode 100644 index 000000000..84fd55d59 --- /dev/null +++ b/app/src/main/res/layout/fancydropdown.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fancydropdown_item.xml b/app/src/main/res/layout/fancydropdown_item.xml new file mode 100644 index 000000000..cad53f755 --- /dev/null +++ b/app/src/main/res/layout/fancydropdown_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_album.xml b/app/src/main/res/layout/grid_item_album.xml new file mode 100644 index 000000000..0661f8ccd --- /dev/null +++ b/app/src/main/res/layout/grid_item_album.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_artist.xml b/app/src/main/res/layout/grid_item_artist.xml new file mode 100644 index 000000000..04b108d5b --- /dev/null +++ b/app/src/main/res/layout/grid_item_artist.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_playlist.xml b/app/src/main/res/layout/grid_item_playlist.xml new file mode 100644 index 000000000..e77dc37e8 --- /dev/null +++ b/app/src/main/res/layout/grid_item_playlist.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_resolver.xml b/app/src/main/res/layout/grid_item_resolver.xml new file mode 100644 index 000000000..bd3b484ba --- /dev/null +++ b/app/src/main/res/layout/grid_item_resolver.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_station.xml b/app/src/main/res/layout/grid_item_station.xml new file mode 100644 index 000000000..8ff6dcfba --- /dev/null +++ b/app/src/main/res/layout/grid_item_station.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_item_user.xml b/app/src/main/res/layout/grid_item_user.xml new file mode 100644 index 000000000..e34e38840 --- /dev/null +++ b/app/src/main/res/layout/grid_item_user.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/hatchet_login_register.xml b/app/src/main/res/layout/hatchet_login_register.xml new file mode 100644 index 000000000..bc1345ee4 --- /dev/null +++ b/app/src/main/res/layout/hatchet_login_register.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_create_playlist.xml b/app/src/main/res/layout/imageview_create_playlist.xml new file mode 100644 index 000000000..63bb593ef --- /dev/null +++ b/app/src/main/res/layout/imageview_create_playlist.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_one.xml b/app/src/main/res/layout/imageview_grid_one.xml new file mode 100644 index 000000000..61180d742 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_one.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_one_content_header.xml b/app/src/main/res/layout/imageview_grid_one_content_header.xml new file mode 100644 index 000000000..0434bbb80 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_one_content_header.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_three.xml b/app/src/main/res/layout/imageview_grid_three.xml new file mode 100644 index 000000000..f6d1297e6 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_three.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_three_content_header.xml b/app/src/main/res/layout/imageview_grid_three_content_header.xml new file mode 100644 index 000000000..a086f1015 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_three_content_header.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_two.xml b/app/src/main/res/layout/imageview_grid_two.xml new file mode 100644 index 000000000..76d6979f9 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_two.xml @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_grid_two_content_header.xml b/app/src/main/res/layout/imageview_grid_two_content_header.xml new file mode 100644 index 000000000..7dbd24c79 --- /dev/null +++ b/app/src/main/res/layout/imageview_grid_two_content_header.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/imageview_station_genre.xml b/app/src/main/res/layout/imageview_station_genre.xml new file mode 100644 index 000000000..7107795ef --- /dev/null +++ b/app/src/main/res/layout/imageview_station_genre.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_header_socialaction.xml b/app/src/main/res/layout/list_header_socialaction.xml new file mode 100644 index 000000000..22267c8fb --- /dev/null +++ b/app/src/main/res/layout/list_header_socialaction.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_header_socialaction_fake.xml b/app/src/main/res/layout/list_header_socialaction_fake.xml new file mode 100644 index 000000000..0ca73d48f --- /dev/null +++ b/app/src/main/res/layout/list_header_socialaction_fake.xml @@ -0,0 +1,45 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_album.xml b/app/src/main/res/layout/list_item_album.xml new file mode 100644 index 000000000..73a1d93c9 --- /dev/null +++ b/app/src/main/res/layout/list_item_album.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_artist.xml b/app/src/main/res/layout/list_item_artist.xml new file mode 100644 index 000000000..fd889be81 --- /dev/null +++ b/app/src/main/res/layout/list_item_artist.xml @@ -0,0 +1,49 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_folder.xml b/app/src/main/res/layout/list_item_folder.xml new file mode 100644 index 000000000..dc2c85cfc --- /dev/null +++ b/app/src/main/res/layout/list_item_folder.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_folder_header.xml b/app/src/main/res/layout/list_item_folder_header.xml new file mode 100644 index 000000000..4d2fe59b3 --- /dev/null +++ b/app/src/main/res/layout/list_item_folder_header.xml @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_image.xml b/app/src/main/res/layout/list_item_image.xml new file mode 100644 index 000000000..4aceabd6d --- /dev/null +++ b/app/src/main/res/layout/list_item_image.xml @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_numeration_track_artist.xml b/app/src/main/res/layout/list_item_numeration_track_artist.xml new file mode 100644 index 000000000..1d4b425f6 --- /dev/null +++ b/app/src/main/res/layout/list_item_numeration_track_artist.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_numeration_track_duration.xml b/app/src/main/res/layout/list_item_numeration_track_duration.xml new file mode 100644 index 000000000..9a8b2f9f6 --- /dev/null +++ b/app/src/main/res/layout/list_item_numeration_track_duration.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_resolver_imageview.xml b/app/src/main/res/layout/list_item_resolver_imageview.xml new file mode 100644 index 000000000..82087006c --- /dev/null +++ b/app/src/main/res/layout/list_item_resolver_imageview.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_text.xml b/app/src/main/res/layout/list_item_text.xml new file mode 100644 index 000000000..54e52be90 --- /dev/null +++ b/app/src/main/res/layout/list_item_text.xml @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_text_highlighted.xml b/app/src/main/res/layout/list_item_text_highlighted.xml new file mode 100644 index 000000000..483c008ff --- /dev/null +++ b/app/src/main/res/layout/list_item_text_highlighted.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_track_artist.xml b/app/src/main/res/layout/list_item_track_artist.xml new file mode 100644 index 000000000..41ecae68b --- /dev/null +++ b/app/src/main/res/layout/list_item_track_artist.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_track_artist_queued.xml b/app/src/main/res/layout/list_item_track_artist_queued.xml new file mode 100644 index 000000000..0c0d9f38f --- /dev/null +++ b/app/src/main/res/layout/list_item_track_artist_queued.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_user.xml b/app/src/main/res/layout/list_item_user.xml new file mode 100644 index 000000000..aa195f3cc --- /dev/null +++ b/app/src/main/res/layout/list_item_user.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listview.xml b/app/src/main/res/layout/listview.xml new file mode 100644 index 000000000..2fc4ff79e --- /dev/null +++ b/app/src/main/res/layout/listview.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/menu_header_cloudcollection.xml b/app/src/main/res/layout/menu_header_cloudcollection.xml new file mode 100644 index 000000000..5ff5bb3e3 --- /dev/null +++ b/app/src/main/res/layout/menu_header_cloudcollection.xml @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/notification_large.xml b/app/src/main/res/layout/notification_large.xml new file mode 100644 index 000000000..659988962 --- /dev/null +++ b/app/src/main/res/layout/notification_large.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/notification_small.xml b/app/src/main/res/layout/notification_small.xml new file mode 100644 index 000000000..7954c17c4 --- /dev/null +++ b/app/src/main/res/layout/notification_small.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/notification_small_compat.xml b/app/src/main/res/layout/notification_small_compat.xml new file mode 100644 index 000000000..b020f0c53 --- /dev/null +++ b/app/src/main/res/layout/notification_small_compat.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/page_indicator_item.xml b/app/src/main/res/layout/page_indicator_item.xml new file mode 100644 index 000000000..aa5da71bc --- /dev/null +++ b/app/src/main/res/layout/page_indicator_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/page_indicator_spacer.xml b/app/src/main/res/layout/page_indicator_spacer.xml new file mode 100644 index 000000000..fcb613e3d --- /dev/null +++ b/app/src/main/res/layout/page_indicator_spacer.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pagerfragment_layout.xml b/app/src/main/res/layout/pagerfragment_layout.xml new file mode 100644 index 000000000..e64b870f2 --- /dev/null +++ b/app/src/main/res/layout/pagerfragment_layout.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playback_fragment.xml b/app/src/main/res/layout/playback_fragment.xml new file mode 100644 index 000000000..e79f149f4 --- /dev/null +++ b/app/src/main/res/layout/playback_fragment.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playback_panel.xml b/app/src/main/res/layout/playback_panel.xml new file mode 100644 index 000000000..c764048e3 --- /dev/null +++ b/app/src/main/res/layout/playback_panel.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/playbackfragment_navigation_coachmark.xml b/app/src/main/res/layout/playbackfragment_navigation_coachmark.xml new file mode 100644 index 000000000..6d9592547 --- /dev/null +++ b/app/src/main/res/layout/playbackfragment_navigation_coachmark.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playbackpanel_seek_coachmark.xml b/app/src/main/res/layout/playbackpanel_seek_coachmark.xml new file mode 100644 index 000000000..fa7225617 --- /dev/null +++ b/app/src/main/res/layout/playbackpanel_seek_coachmark.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playlistsfragment_layout.xml b/app/src/main/res/layout/playlistsfragment_layout.xml new file mode 100644 index 000000000..4feb9cbf0 --- /dev/null +++ b/app/src/main/res/layout/playlistsfragment_layout.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/progressbar.xml b/app/src/main/res/layout/progressbar.xml new file mode 100644 index 000000000..1077b5ed9 --- /dev/null +++ b/app/src/main/res/layout/progressbar.xml @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/progressbar_white.xml b/app/src/main/res/layout/progressbar_white.xml new file mode 100644 index 000000000..b10bbda8e --- /dev/null +++ b/app/src/main/res/layout/progressbar_white.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_container.xml b/app/src/main/res/layout/row_container.xml new file mode 100644 index 000000000..9ceda2a0b --- /dev/null +++ b/app/src/main/res/layout/row_container.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_container_spacer.xml b/app/src/main/res/layout/row_container_spacer.xml new file mode 100644 index 000000000..681729b8b --- /dev/null +++ b/app/src/main/res/layout/row_container_spacer.xml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/searchview_dropdown_item.xml b/app/src/main/res/layout/searchview_dropdown_item.xml new file mode 100644 index 000000000..f94f2f189 --- /dev/null +++ b/app/src/main/res/layout/searchview_dropdown_item.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/selector.xml b/app/src/main/res/layout/selector.xml new file mode 100644 index 000000000..446dc20f6 --- /dev/null +++ b/app/src/main/res/layout/selector.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/selectorfragment_item.xml b/app/src/main/res/layout/selectorfragment_item.xml new file mode 100644 index 000000000..00a9b553d --- /dev/null +++ b/app/src/main/res/layout/selectorfragment_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/selectorfragment_layout.xml b/app/src/main/res/layout/selectorfragment_layout.xml new file mode 100644 index 000000000..2cdc1a2ff --- /dev/null +++ b/app/src/main/res/layout/selectorfragment_layout.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/simplepagertabs_tab_divider.xml b/app/src/main/res/layout/simplepagertabs_tab_divider.xml new file mode 100644 index 000000000..cd4448f07 --- /dev/null +++ b/app/src/main/res/layout/simplepagertabs_tab_divider.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/simplepagertabs_tab_indicator.xml b/app/src/main/res/layout/simplepagertabs_tab_indicator.xml new file mode 100644 index 000000000..247037dba --- /dev/null +++ b/app/src/main/res/layout/simplepagertabs_tab_indicator.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/simplepagertabs_tab_item.xml b/app/src/main/res/layout/simplepagertabs_tab_item.xml new file mode 100644 index 000000000..8d4877c65 --- /dev/null +++ b/app/src/main/res/layout/simplepagertabs_tab_item.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/single_line_list_header.xml b/app/src/main/res/layout/single_line_list_header.xml new file mode 100644 index 000000000..535734310 --- /dev/null +++ b/app/src/main/res/layout/single_line_list_header.xml @@ -0,0 +1,47 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/single_line_list_item.xml b/app/src/main/res/layout/single_line_list_item.xml new file mode 100644 index 000000000..19ec8627a --- /dev/null +++ b/app/src/main/res/layout/single_line_list_item.xml @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/single_line_list_menu.xml b/app/src/main/res/layout/single_line_list_menu.xml new file mode 100644 index 000000000..0b84da7e6 --- /dev/null +++ b/app/src/main/res/layout/single_line_list_menu.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/spinner_country_code.xml b/app/src/main/res/layout/spinner_country_code.xml new file mode 100644 index 000000000..2602fc2d4 --- /dev/null +++ b/app/src/main/res/layout/spinner_country_code.xml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/spinner_dropdown_textview.xml b/app/src/main/res/layout/spinner_dropdown_textview.xml new file mode 100644 index 000000000..410544194 --- /dev/null +++ b/app/src/main/res/layout/spinner_dropdown_textview.xml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/spinner_dropdown_textview_country_code.xml b/app/src/main/res/layout/spinner_dropdown_textview_country_code.xml new file mode 100644 index 000000000..64c0ed757 --- /dev/null +++ b/app/src/main/res/layout/spinner_dropdown_textview_country_code.xml @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/spinner_textview.xml b/app/src/main/res/layout/spinner_textview.xml new file mode 100644 index 000000000..d0c73e3b3 --- /dev/null +++ b/app/src/main/res/layout/spinner_textview.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/spinner_textview_country_code.xml b/app/src/main/res/layout/spinner_textview_country_code.xml new file mode 100644 index 000000000..ec4fafb31 --- /dev/null +++ b/app/src/main/res/layout/spinner_textview_country_code.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/swipe_menu_button_dequeue.xml b/app/src/main/res/layout/swipe_menu_button_dequeue.xml new file mode 100644 index 000000000..6e58b6441 --- /dev/null +++ b/app/src/main/res/layout/swipe_menu_button_dequeue.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/swipe_menu_button_enqueue.xml b/app/src/main/res/layout/swipe_menu_button_enqueue.xml new file mode 100644 index 000000000..539e39e42 --- /dev/null +++ b/app/src/main/res/layout/swipe_menu_button_enqueue.xml @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/swipelayout_enqueue_coachmark.xml b/app/src/main/res/layout/swipelayout_enqueue_coachmark.xml new file mode 100644 index 000000000..ef95c5930 --- /dev/null +++ b/app/src/main/res/layout/swipelayout_enqueue_coachmark.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tomahawk_main_activity.xml b/app/src/main/res/layout/tomahawk_main_activity.xml new file mode 100644 index 000000000..dfcbee23c --- /dev/null +++ b/app/src/main/res/layout/tomahawk_main_activity.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/tomahawklistfragment_layout.xml b/app/src/main/res/layout/tomahawklistfragment_layout.xml new file mode 100644 index 000000000..b2270e2ed --- /dev/null +++ b/app/src/main/res/layout/tomahawklistfragment_layout.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/web_view_activity.xml b/app/src/main/res/layout/web_view_activity.xml new file mode 100644 index 000000000..93d2582ce --- /dev/null +++ b/app/src/main/res/layout/web_view_activity.xml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment.xml b/app/src/main/res/layout/welcome_fragment.xml new file mode 100644 index 000000000..f88edcd84 --- /dev/null +++ b/app/src/main/res/layout/welcome_fragment.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment_page_done.xml b/app/src/main/res/layout/welcome_fragment_page_done.xml new file mode 100644 index 000000000..400ac294c --- /dev/null +++ b/app/src/main/res/layout/welcome_fragment_page_done.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment_page_explanation.xml b/app/src/main/res/layout/welcome_fragment_page_explanation.xml new file mode 100644 index 000000000..ee0bc4ecb --- /dev/null +++ b/app/src/main/res/layout/welcome_fragment_page_explanation.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment_page_hatchet.xml b/app/src/main/res/layout/welcome_fragment_page_hatchet.xml new file mode 100644 index 000000000..95a457bd6 --- /dev/null +++ b/app/src/main/res/layout/welcome_fragment_page_hatchet.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_fragment_page_setup.xml b/app/src/main/res/layout/welcome_fragment_page_setup.xml new file mode 100644 index 000000000..fdcb4be26 --- /dev/null +++ b/app/src/main/res/layout/welcome_fragment_page_setup.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/tomahawk_main_menu.xml b/app/src/main/res/menu/tomahawk_main_menu.xml new file mode 100644 index 000000000..cfc02de73 --- /dev/null +++ b/app/src/main/res/menu/tomahawk_main_menu.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..033b78593 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,276 @@ + + + حسنًا + ألغِ + لِج + اخرج + سجّل + سجّل ولِج + أعد الفحص الآن + التّخزين الدّاخليّ + المقطوعات + الألبومات + الفنّانون + المستخدمون + أعلى النّقرات + أعلى الألبومات + إصدارات أخرى + تفاصيل قائمة التّشغيل + تفاصيل الألبوم + ابحث عن + قائمة تشغيل + الخلط يعمل + الخلط لا يعمل + التّكرار يعمل + التّكرار لا يعمل + مجهول + لا مقطوعة + احفظ قائمة التّشغيل الحاليّة + أنشئ قائمة تشغيل + + العنوان + البريد الإلكترونيّ/اسم المستخدم + اسم المستخدم + كلمة المرور + أكّد كلمة المرور + البريد الإلكترونيّ (اختياريّ) + هذا الحقل مطلوب + كلمات المرور لا تتطابق + اسم المستخدم أو كلمة المرور غير صالحة. + رفض الخادومُ الحسابَ. + تعذّر الاستيثاق. فضلًا تحقّق من اتّصالك. + الإجراء غير مسموح به، الحساب مستخدم في مكان ما. + انتهت صلاحيّة حسابك. + + ولجت إلى %1$s + + خرجت من %1$s + التّجميعة المحليّة + المحليّ + الكلّ + اختر الأدلّة الّتي يجب فحص الوسائط المحليّة فيها. + زامن بياناتك مع %1$s. + التّشغيل + المعادل + خصّص خرج الصّوت. + انقل كلّ شيء + انقل كلّ مقطوعة من كلّ مشغّل موسيقى في الهاتف إلى %1$s. + أوصل للتّشغيل + يبدأ تشغيل الموسيقى بمجرّد اتّصال سمّاعة الرأس بلاقط صوت. + جودة الصّوت المحبّذة + جودة الصّوت المحبّذة عند عدم الاتّصال بِواي-⁠فاي. + معلومات + إصدارة التّطبيق + التّغذية الاسترجاعيّة + شارك أفكارك أو اسأل عن الدّعم. + أرسل سجلًّا + أرسل ملفّ السّجلّ لأغراض تنقيحيّة. + فضلًا صِف بإيجاز المشكلة الّتي تواجهها ووفّر بريدًا إلكترونيًّا لنتّصل بك إن لزم الأمر. شكرًا! + عنوان بريدك الإلكترونيّ + المشكلة + انهار توماهوك + حدث خطأ غير متوقّع أجبر التّطبيق على التوقّف. فضلًا ساعدنا لإصلاح هذا بإرسال بيانات الخطأ. كلّ ما عليك فعله هو نقر حسنًا. + يمكنك إضافة تعليقات حول المشكلة أدناه: + شكرًا لك! + شغّل + أضف إلى الاصطفاف + أضف إلى قائمة تشغيل + احذف + أزل من قائمة التّشغيل + + أزل من التّجميعة + + أضف إلى التّجميعة + + أحبّها + + لا أحبّها + شارك + عليك الولوج إلى Hatchet لتشارك قائمة التّشغيل هذه. + التّغذية + التّجميعة + قوائم التّشغيل + المفضّلة + إعدادات + تجميعات السّحاب + المضافة حديثًا + الفنّان أ-ي + عدد مرّات التّشغيل + الألبوم أ-ي + أ-ي + التّأريخ + عاديّة + جيّدة + ممتازة + افتح درج التّنقّل + أغلق درج التّنقّل + + متابَع + + تابع + + أشخاص مقترح متابعتهم + + يتابع %1$s الآن + + + لم يحبّ %1$s أيّ مقطوعة + أحبّ %1$s مقطوعة واحدة + أحبّ %1$s مقطوعتان + أحبّ %1$s ‏%2$d مقطوعات + أحبّ %1$s ‏%2$d مقطوعةً + أحبّ %1$s ‏%2$d مقطوعة + + + + لم يجمع %1$s أيّ مقطوعة + جمع %1$s مقطوعة واحدة + جمع %1$s مقطوعتان + جمع %1$s ‏%2$d مقطوعات + جمع %1$s ‏%2$d مقطوعةً + جمع %1$s ‏%2$d مقطوعة + + + + لم يجمع %1$s أيّ ألبوم + جمع %1$s ألبومًا واحدًا + جمع %1$s ألبومان + جمع %1$s ‏%2$d ألبومات + جمع %1$s ‏%2$d ألبومًا + جمع %1$s ‏%2$d ألبوم + + + + لم يجمع %1$s أيّ فنّان + جمع %1$s فنّانًا واحدًا + جمع %1$s فنّانان + جمع %1$s ‏%2$d فنّانين + جمع %1$s ‏%2$d فنّانًا + جمع %1$s ‏%2$d فنّان + + + علّق %1$s على + + + لم يستمع %1$s إلى أيّ صديق + استمع %1$s إلى صديق واحد + استمع %1$s إلى صديقان + استمع %1$s إلى %2$d أصدقاء + استمع %1$s إلى %2$d صديقًا + استمع %1$s إلى %2$d صديق + + + + لم ينشئ%1$s أيّ قائمة تشغيل + أنشأ %1$s قائمة تشغيل واحدة + أنشأ %1$s قائمتا تشغيل + أنشئ %1$s ‏%2$d قوائم تشغيل + أنشئ %1$s ‏%2$d قائمة تشغيل + أنشئ %1$s ‏%2$d قائمة تشغيل + + + %1$s لِـ %2$s + قبل بضع ثوان + + قبل بضعة ثوان + قبل دقيقة واحدة + قبل دقيقتان + قبل %1$d دقائق + قبل %1$d دقيقةً + قبل %1$d دقيقة + + + قبل بضعة دقائق + قبل ساعة واحدة + قبل ساعتان + قبل %1$d ساعات + قبل %1$d ساعةً + قبل %1$d ساعة + + + قبل بضعة اعات + قبل يوم واحد + قبل يومان + قبل %1$d أيّام + قبل %1$d يومًا + قبل %1$d يوم + + تشغيلات %1$s الحديثة + مفضّلة %1$s + قائمة تشغيل %1$s + الموسيقى + السّيرة الذّاتيّة + + المشابهة + + الأغاني + + + لا أغاني + أغنية واحدة + أغنيتان + %1$d أغاني + %1$d أغنيةً + %1$d أغنية + + النّشاط + + المتابِعين + + المتابَعين + + المتابِعين: %1$d، المتابَعين: %2$d + لِج إلى + اخرج من + نزّل ملحقة لِـ + أتريد حفظ بيانات كلّ المقطوعات التي تشغّلها في Hatchet آليًّا؟ + سنفعل ذلك لك! فضلًا مكّن خدمتنا في إعدادات النّظام. + أغلق + اعرض الألبوم + الاتّصال + اختر الخدمات الّتي تريد استخدامها. يعمل توماهوك أفضل عندما تكون خدمات أكثر مفعّلة. + مثبّت يدويًّا + متقدّم + معلومات + يمكنك السّعي في مقطوعة بالضّغط الطّويل على زرّ التّشغيل. + المس لإضافة مقطوعة إلى الاصطفاف. + اسحب لأعلى لرؤية قائمة التّشغيل الحاليّة. + اسحب لأسفل لإغلاق درج التّشغيل. + مرّر لليسار/لليمين كي تنتقل إلى المقطوعة التّالية/السّابقة. انقر مطوّلًا لفتح قائمة السّياق. انقر مزدوجًا للإضافة إلى المفضّلة + + التّالي + البدء + الموسيقى في كلّ مكان.\nليس عليك أن تكون كذلك الآن. + توقّف عن ملاحقة الموسيقى في الوِبّ وفي الخدمات الّتي لا تستخدمها، أو المصادر الّتي لا تملك نفاذًا إليها. قدّم اسم الأغنية أو وصلة إليها، أو ألبوم أو قائمة تشغيل، وسيعثر توماهوك على أفضل مصدر متوفّر ويشغّله. + أتريد أن تكون اجتماعيًّا؟ + أخيرًا، يمكنك عبر Hatchet امتلاك تجميعة موسيقى في مكان واحد، أو مشاركة قوائم التّشغيل أو المقطوعات المفضّلة، أو ببساطة مزامنة التّطبيق بتاماهوك على سطح المكتب. + اتّصل + تمّ! + تمّ كلّ شيء! + استمتع باستكشاف التّطبيق وتأكّد من إخبارنا بكلّ ما تفكّر به! + + اختر قائمة تشغيل لعدم إضافة أيّ مقطوعات إليها: + اختر قائمة تشغيل لإضافة مقطوعة واحدة إليها: + اختر قائمة تشغيل لإضافة مقطوعتان إليها: + اختر قائمة تشغيل لإضافة %1$d مقطوعات إليها: + اختر قائمة تشغيل لإضافة %1$d مقطوعةً إليها: + اختر قائمة تشغيل لإضافة %1$d مقطوعة إليها: + + آسفون، لا يدعم هذا الحلّالُ السّعيَ. + تثبيت الملحقات + ستثبّت ملحقة توماهوك من مصدر مجهول. يمكن أن ستسبّب هذا بخطر أمنيّ. + أتريد حقًّا تثبيت هذه الملحقة؟ + إزالة تثبيت الملحقات + أتريد حقًّا إزالة تثبيت هذه الملحقة؟ + اختر حسابًا: + تعذّر فتح الملفّ. + ألبوم مجهول + فنّان مجهول + تعذّر العثور على تطبيق الإعدادات في جهازك. فضلًا أعطِ توماهوك النّفاذ إلى إخطاراتك في إعدادات النّظام. + تحذير: أنت على وشك تنزيل ملحقة تحوي أجزاء مغلقة المصدر. + تطبيق الملحقة المطلوب قديم ولم يعد متوافقًا. أتريد التّحديث إليه الآن؟ + تطبيق الملحقة المطلوب لم يعد مثبّتًا. أتريد تثبيته الآن؟ + + + + diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml new file mode 100644 index 000000000..c93e3d3dc --- /dev/null +++ b/app/src/main/res/values-ast/strings.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..d05cd6fcc --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,274 @@ + + + OK + Zrušit + Přihlášení + Odhlášení + Registrace + Registrace & přihlášení + Prohledat nyní znovu + Vnitřní úložiště + Skladby + Alba + Umělci + Uživatelé + Nejlepší písně + Nejlepší alba + Alba a EP + Jiná vydání + Podrobnosti seznamu skladeb + Podrobnosti alba + Hledat + Seznam skladeb + Míchání zapnuto + Míchání vypnuto + Opakování zapnuto + Opakování vypnuto + neznámý + žádná skladba + Uložit nynější seznam skladeb + Vytvořil seznam skladeb + + Název + Vytvořil stanici + Hledat umělce/písně/žánry + Žánry + Nálady + E-Mail/Uživatelské jméno + Uživatelské jméno + Heslo + Potvrzení hesla + E-Mail (volitelné) + Toto pole je požadováno + Heslo nesouhlasí + Uživatelské jméno nebo heslo neodpovídá. + Účet odmítnut serverem. + Nelze ověřit. Prověřte, prosím, své připojení. + Činnost nepovolena. Účet je již používán. + Váš účet vypršel. + + Přihlášen k %1$s + + Odhlášen z %1$s + Místní sbírka + Místní + Vše + Vyberte adresáře, které se mají prohledat kvůli nalezení místních souborů. + Seřídit data s %1$s. + Přehrávání + Ekvalizér + Přizpůsobit zvukový výstup. + Odesílat údaje o všem + Odesílat údaje o každé skladbě z každého hudebního přehrávače v telefonu %1$s. + Zastrčit pro přehrávání + Začne s přehráváním hudby, jakmile jsou sluchátka připojena. + Upřednostňovaná jakost zvuku + Upřednostňovaná jakost zvuku, pokud není připojeno k WiFi. + + Informace + Verze + Zpětná vazba + Sdílejte své myšlenky a požádejte o podporu. + Hodnotit aplikaci + Hodnoťte Tomahawk na Google Play Store. + Navštivte naše stránky + Otevřete tomahawk-player.org v prohlížeči. + Poslat záznam + Poslat váš soubor se záznamem pro účely ladění. + Popište, prosím, krátce vaše potíže a poskytněte adresu elektronické pošty, abychom se s v případě potřeby vámi mohli spojit. Díky! + Vaše elektronická poštovní adresa + Potíže + Tomahawk spadl + Neočekávaná chyba vedla k zastavení programu. Pomozte nám ji, prosím, opravit tím, že pošlete údaje o chybě. Vše, co musíte udělat, je klepnout na OK. + Níže můžete přidat své poznámky o potížích: + Děkujeme! + Přehrát + Zařadit + Přidat do seznamu skladeb + Vytvořil stanici + Smazat + Odstranit ze seznamu skladeb + + Odstranit ze sbírky + + Přidat do sbírky + + Oblíbit + + Zrušit oblibu skladby + Sdílet + Musíte se přihlásit k Hatchet, abyste mohl sdílet tento seznam skladeb. + Kanál + Grafy + Sbírka + Seznamy skladeb + Stanice + Oblíbené + Nastavení + Sbírky na serveru + Nedávno přidáno + Umělec A-Z + Počet přehrání + Album A-Z + A-Z + Historie + Normální + Dobré + Nejlepší + Otevřít kartu pro pohyb + Zavřít kartu pro pohyb + + Sleduje + + Sledovat + + Navržení lidé ke sledování + + %1$s nyní sleduje + + + %1$s si oblíbil skladbu + %1$s si oblíbil %2$d skladby + %1$s si oblíbil %2$d skladeb + + + + %1$s sebral skladbu + %1$s sebral %2$d skladby + %1$s sebral %2$d skladeb + + + + %1$s sebral album + %1$s sebral %2$d alba + %1$s sebral %2$d alb + + + + %1$s sebral umělce + %1$s sebral %2$d umělce + %1$s sebral %2$d umělců + + + %1$s přidal poznámku k + + + %1$s poslouchal přítele + %1$s poslouchal %2$d přátele + %1$s poslouchal %2$d přátel + + + + %1$s vytvořil seznam skladeb + %1$s vytvořil %2$d seznamy skladeb + %1$s vytvořil %2$d seznamů skladeb + + + %1$s od %2$s + před několika sekundami + + před minutou + před %1$d minutami + před %1$d minutami + + + před hodinou + před %1$d hodinami + před %1$d hodinami + + + před dnem + před %1$d dny + před %1$d dny + + %1$s nedávno hráno + %1$s oblíbení + %1$s seznam skladeb + Moje nedávno hrané + Moje oblíbené + Můj seznam skladeb + Hudba + Životopis + + Podobní + + Písně + + + %1$d píseň + %1$d písně + %1$d písní + + Činnost + + Sledující + + Sleduje + + Sledující: %1$d, sleduje: %2$d + Povolit + Zakázat + Přihlásit se + Odhlásit se + Stáhnout přídavný modul pro + Chcete automaticky odesílat informace o všech skladbách, které jsou přehrávány, Hatchet? + Uděláme to za vás! povolte, prosím, naši službu v nastavení sytému. + Zavřít + Zobrazit album + Připojit + Vyberte služby, jež hodláte používat. Tomahawk pracuje lépe, když je zapnuto více služeb. + Nainstalováno ručně + Pokročilé + Informace + Můžete skladbu prohledávat za pomoci dlouhého stisknutí tlačítka pro přehrávání. + Dotkněte se pro přidání skladby do řady. + Stáhněte dolů, abyste viděli nynější seznam skladeb. + Stáhněte dolů, abyste zavřeli tažec přehrávání. + Přejetím prstem doleva/doprava přeskočíte na další/předchozí skladbu. Podržte prst pro otevření kontextové nabídky. Klepněte dvakrát pro přidání do oblíbených. + + Další + Úvod + Hudba je všude. +Vy teď nemusíte. + Přestaňte naháňet hudbu po celém internetu - po službách, které nepoužíváte, ve zdrojích, ke kterým nemáte přístup. Vložte název nebo odkaz na píseň, album nebo seznam skladeb a Tomahawk pro vás najde ty nejlepší zdroje a prostě je přehraje. + Chcete se stýkat? + S Hatchet můžete mít konečně celou vaši hudební sbírku na jednom místě. Sdílejte vaše seznamy skladeb a oblíbené skladby. Nebo prostě aplikaci synchronizujte s vaším Tomahawkem na počítači. + Připojit + Hotovo! + Vše hotovo! + Užijte si objevování aplikace a určitě nám povězte, co si o ní myslíte! + + Vyberte seznam skladeb, do něhož se má přidat %1$d skladba: + Vyberte seznam skladeb, do něhož se mají přidat %1$d skladby: + Vyberte seznam skladeb, do něhož se má přidat %1$d skladeb: + + Promiňte, ale tento řešitel nepodporuje hledání. + Instalace přídavného modulu + Chystáte senainstalovat přídavný modul pro Tomahawk z neznámého zdroje. V tom může být bezpečnostní nebezpečí. + Opravdu chcete nainstalovat tento přídavný modul? + Odinstalovává se přídavný modul + Opravdu chcete odinstalovat tento přídavný modul? + Vyberte účet: + Nelze otevřít soubor. + Neznámé album + Neznámý umělec + Na vašem zařízení se nepodařilo najít nastavení aplikace. Dejte, prosím, Tomahawku přístup k vašemu oznamování v nastavení systému. + Upozornění: Chystáte se stáhnout přídavný modul obsahující části s uzavřeným (\"nesvobodným\") kódem. + Požadováný program s přídavným modulem je zastaralý a už není slučitelný. Chcete jej nyní aktualizovat? + Požadováný program s přídavným modulem už není nainstalován. Chcete jej nyní nainstalovat? + + nový + Pozastavit + Přehrát + Předchozí skladba + Další skladba + Přidat do oblíbených + Odstranit z oblíbených + + Nyní hraje + + Zamíchat a přehrát + Nahrání stanice: + Navržení stanice + Moje stanice + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..5f113ef05 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,261 @@ + + + OK + Abbrechen + Anmelden + Abmelden + Registrieren + Registrieren & Anmelden + Jetzt durchsuchen + Interner Speicher + Lieder + Alben + Künstler + Benutzer + Top Hits + Top Alben + Alben und EPs + Andere Veröffentlichungen + Playlist Details + Album Details + Suche + Playlist + Zufallswiedergabe ist an + Zufallswiedergabe ist aus + Wiederholen ist an + Wiederholen ist aus + unbekannt + kein Lied + Aktuelle Playlist speichern + Playlist erstellen + + Titel + Station erstellen + Suche nach Künstlern/Liedern/Genres + Genres + Stimmungen + E-Mail/Benutzername + Benutzername + Passwort + Passwort bestätigen + E-Mail (optional) + Dieses Feld ist erforderlich + Passwörter stimmen nicht überein + Benutzername oder Passwort falsch. + Konto wurde vom Server zurückgewiesen. + Authentifizierung nicht möglich. Bitte überprüfe deine Verbindung. + Aktion nicht erlaubt, das Konto ist woanders in Benutzung. + Dein Account ist abgelaufen. + + Angemeldet bei %1$s + + Abgemeldet von %1$s + Lokale Sammlung + Lokal + Alle + Wähle die Ordner, in denen nach Musik gesucht werden soll. + Synchronisiere deine Daten mit %1$s. + Wiedergabe + Equalizer + Audio-Ausgabe anpassen + Scrobble alles + Scrobble jedes Lied von allen deiner Musik-Apps auf deinem Smartphone zu %1$s. + Abspielen beim Anschließen + Spielt Musik ab, sobald ein Headset angeschlossen ist. + Bevorzugte Audioqualität + Die bevorzugte Audioqualität, wenn keine WLAN-Verbindung besteht. + Info + App-Version + Feedback + Teile deine Ideen oder frag nach Hilfe. + Bewerte die App + Bewerte Tomahawk im Google Play Store + Besuche unsere Webseite + Öffne tomahawk-player.org in deinem Browser + Protokoll senden + Versende die Protokolldatei zu Debugzwecken. + Bitte beschreibe kurz dein Problem und gib eine E-Mail an, damit wir dich gegebenenfalls kontaktieren können. Danke! + Deine E-Mail-Adresse + Problem + Tomahawk ist abgestürzt + Ein unerwarteter Fehler ist aufgetreten und hat die Anwendung beendet. Bitte hilf uns den Fehler zu beheben, indem du uns den Absturzbericht durch einen Klick auf OK zusendest. + Wenn du möchtest, kannst du einen Kommentar zu diesem Problem hinzufügen: + Vielen Dank! + Abspielen + Zur Warteschlange hinzufügen + Zu Playlist hinzufügen + Station erstellen + Löschen + Aus der Playlist entfernen + + Aus der Sammlung entfernen + + Zur Sammlung hinzufügen + + Zu den Favoriten hinzufügen + + Aus den Favoriten entfernen + Teilen + Bitte logge dich in Hatchet ein, um diese Playlist teilen zu können + Feed + Charts + Sammlung + Playlists + Stationen + Favoriten + Einstellungen + Cloud Sammlungen + Kürzlich hinzugefügt + Künstler A-Z + Wiedergabeanzahl + Album A-Z + A-Z + Chronik + Normal + Gut + Beste + Navigationsmenü öffnen + Navigationsmenü schließen + + Folge ich + + Folgen + + Zum Folgen vorgeschlagene Personen + + %1$s folgt nun + + + %1$s hat ein Lied zu den Favoriten hinzugefügt + %1$s hat %2$d Lieder zu den Favoriten hinzugefügt + + + + %1$s hat ein Lied zur Sammlung hinzugefügt + %1$s hat %2$d Lieder zur Sammlung hinzugefügt + + + + %1$s hat ein Album zur Sammlung hinzugefügt + %1$s hat %2$d Alben zur Sammlung hinzugefügt + + + + %1$s hat einen Künstler zur Sammlung hinzugefügt + %1$s hat %2$d Künstler zur Sammlung hinzugefügt + + + %1$s hat kommentiert + + + %1$s hat einem Freund zugehört + %1$s hat %2$d Freunden zugehört + + + + %1$s hat eine Playlist erstellt + %1$s hat %2$d Playlists erstellt + + + %1$s von %2$s + vor ein paar Sekunden + + vor einer Minute + vor %1$d Minuten + + + vor einer Stunde + vor %1$d Stunden + + + vor einem Tag + vor %1$d Tagen + + %1$ss kürzlich gespielte Lieder + %1$ss Favoriten + %1$ss Playlist + Meine Abspielverlauf + Meine Favoriten + Meine Playlist + Musik + Biografie + + Ähnliche + + Lieder + + + %1$d Lied + %1$d Lieder + + Aktivitäten + + Followers + + Folgt + + Followers: %1$d, Folgen: %2$d + Aktivieren + Deaktivieren + Anmelden bei + Abmelden von + Lade Plugin herunter für + Möchtest du automatisch alle Lieder, die du gehört hast, auf Hatchet speichern? + Wir machen das für dich! Bitte aktiviere unseren Dienst in deinen System Einstellungen. + Schließen + Album ansehen + Verbinden + Wähle die Dienste aus, die Du verwenden möchtest. Tomahawk funktioniert besser, wenn mehrere Dienste aktiviert sind. + Manuell installiert + Erweitert + Info + Durchsuche den Track durch langes Drücken des Play-Buttons + Tippen, um das Lied zur Warteschlange hinzuzufügen. + Hochziehen, um die aktuelle Playlist zu sehen + Nach unten ziehen, um die Wiedergabe zu schließen + Swipe nach links/rechts, um zum vorherigen/nächsten Titel zu springen. Lang drücken, um das Kontextmenü zu öffnen. Tippe zweimal, um den aktuellen Track zu den Favoriten hinzuzufügen. + + Weiter + Einführung + Musik ist überall.\nJetzt musst du es nicht mehr sein. + Hör auf deiner Musik hinterherzujagen - nur um Dienste zu finden, die du nicht nutzt oder zu denen du keinen Zugang hast. Durch die Eingabe eines Namens oder eines Links zu einem Lied, Album oder zu einer Playlist sucht Tomahawk für dich die beste verfügbare Quelle heraus und spielt die Musik einfach ab. + Lust unter Leute zu kommen? + Mit Hatchet kannst du endlich deine Musiksammlung an einem einzigen Ort haben. Teile deine Playlists und deine Lieblingslieder oder synchronisiere die App mit Tomahawk auf deinem Desktoprechner. + Verbinden + Fertig! + Alles erledigt! + Viel Spaß beim Erkunden der App! Wir freuen uns auf dein Feedback! + + Wähle eine Playlist aus, um %1$d Lied hinzuzufügen: + Wähle eine Playlist aus, um %1$d Lieder hinzuzufügen: + + Entschuldigung, dieser Resolver unterstützt kein Vor- und Zurückspulen. + Plugin Installation + Du bist dabei ein Tomahawk-Plugin aus einer unbekannten Quelle zu installieren. Dies kann ein Sicherheitsrisiko sein. + Möchtest du dieses Plugin wirklich installieren? + Plugin entfernen + Möchtest du dieses Plugin wirklich entfernen? + Wählen Sie ein Konto aus: + Kann Datei nicht öffnen. + Unbekanntes Album + Unbekannter Interpret + Die Einstellungs-App konnte nicht gefunden werden. Bitte gib Tomahawk Zugriff auf deine Benachrichtigungen in deinen Systemeinstellungen. + Achtung: Du bist im Begriff ein Plugin herunterzuladen, welches nicht vollständig open-source ist. + Die benötigte Plugin-Applikation ist veraltet oder nicht länger kompatibel. Möchtest du sie jetzt updaten? + Die benötigte Plugin-Applikation ist nicht mehr installiert. Möchtest du sie jetzt installieren? + + Neu + Pause + Wiedergabe + Vorheriger Titel + Nächster Titel + Zu den Favoriten hinzufügen + Aus den Favoriten entfernen + + Derzeitige Wiedergabe + + Zufallswiedergabe + Lade Station: + Vorgeschlagene Stationen + Meine Stationen + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..0e3fd1f0e --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,214 @@ + + + OK + Ακύρωση + Είσοδος + Έξοδος + Εγγραφή + Εγγραφή & Σύνδεση + Σάρωση τώρα + Εσωτ. αποθηκευτικός χώρος + Κομμάτια + Άλμπουμ + Καλλιτέχνες + Χρήστες + Κορυφαία Τραγούδια + Κορυφαία Άλμπουμ + Στοιχεία Λίστας + Στοιχεία Άλμπουμ + Εύρεση + Λίστα + Η τυχαία αναπαραγωγή είναι ενεργοποιημενη + Η τυχαία αναπαραγωγή είναι απενεργοποιημενη + Η επανάληψη ειναι ενεργοποιημενη + Η επανάληψη ειναι απενεργοποιημενη + άγνωστο + κανένα κομμάτι + Αποθήκευση τρέχουσας λίστας + Δημιουργία λίστας + + Τίτλος + E-mail/Όνομα χρήστη + Όνομα χρήστη + Κωδικός + Επιβεβαίωση κωδικού + E-mail (προαιρετικό) + Αυτό το πεδίο είναι υποχρεωτικό + Οι κωδικοί δεν ταιριάζουν + Όνομα χρήστη ή κωδικός λανθασμένος. + Ο λογαριασμός απορρίφθηκε από τον server. + Η πιστοποίηση απέτυχε. Παρακαλώ ελέγξτε τη σύνδεσή σας. + Δεν επιτρέπεται η ενέργεια, ο λογαριασμός είναι σε χρήση αλλού. + Ο λογαριασμός σας έχει λήξει. + + Συνδέθηκε στο %1$s + + Αποσυνδέθηκε από το %1$s + Τοπική Συλλογή + Τοπικό + Όλα + Επιλέξτε τους καταλόγους που θα πρέπει να ελέγχονται για τοπικά μέσα ενημέρωσης. + Συγχρονισμός δεδομένων με %1$s. + Αναπαραγωγή + Ισοσταθμιστής + Προσαρμογή εξόδου ήχου. + Μετακινηση ολων + Μετακινηση κάθε τραγούδιου από κάθε φορέα μουσικής εφαρμογής σε τηλέφωνο για το %1$s. + Συνδεση για αναπαραγωγη + Έναρξη της αναπαραγωγής μουσικής, μόλις τα ακουστικά είναι συνδεδεμένα. + Προτιμώμενη ποιότητα ήχου + Προτιμώμενη ποιότητα ήχου αν δεν είστε συνδεδεμένοι με το WiFi. + Πληροφορίες + Έκδοση Εφαρμογής + Σχόλια + Μοιραστείτε τις ιδέες σας ή ζητήστε υποστήριξη. + Αποστολή Σύνδεσης + Αποστολή αρχείου καταγραφής για σκοπούς εντοπισμού σφαλμάτων. + Περιγράψτε εν συντομία το πρόβλημα που αντιμετωπίζετε και γραψτε μας ένα email ώστε να μπορέσουμε να επικοινωνήσουμε μαζί σας εάν χρειαστεί. Ευχαριστούμε! + Η ηλεκτρονική σας διεύθυνση + Ζήτημα + Το Tomahawk έχει καταρρεύσει + Παρουσιάστηκε ενα μη αναμενόμενο σφάλμα αναγκάζοντας την εφαρμογή να σταματήσει. Παρακαλούμε να μας βοηθήσετε να το διορθώσουμε στέλνοντας μας τα δεδομένα του σφάλματος, το μόνο που έχετε να κάνετε είναι να κάνετε κλικ στο κουμπί OK. + Μπορείτε να προσθέσετε τα σχόλιά σας για το παρακάτω πρόβλημα: + Σας ευχαριστούμε! + Αναπαραγωγή + Προσθήκη στην αναμονή + Προσθήκη στη λίστα αναπαραγωγής + Διαγραφή + Αφαίρεση από τη λίστα αναπαραγωγής + + Αφαίρεση από τη Συλλογή + + Προσθήκη στη συλλογή + + Αγάπη + + Καθολου Αγάπη + Μοιρασμα + Τροφοδοσία + Συλλογή + Λίστες αναπαραγωγής + Αγαπημένα + Ρυθμίσεις + Cloud Συλλογές + Προστέθηκαν πρόσφατα + Καλλιτέχνης Α-Ω + Αριθμός αναπαραγωγών + Άλμπουμ Α-Ω + Α-Ω + Ιστορικό + Κανονικα + Καλα + Καλύτερα + Άνοιγμα της συρταριέρας πλοήγησης + Κλεισιμο της συρταριέρας πλοήγησης + + Ακολουθα + + Ακολουθήστε + + Προτεινόμενα άτομα για να τα ακολουθήσετε + + %1$s ακολουθουν + + + %1$s αγαπημενο τραγουδι + %1$s αγαπημενα %2$d τραγουδια + + + + %1$s συλλέχθηκε ένα τραγουδι + %1$s συλλεγμενα %2$d τραγουδια + + + + %1$s συλλέχθηκε ενα αλμπουμ + %1$s συλλέχθηκαν %2$d αλμπουμ + + + + %1$s συλλεχθηκε ενας τραγουδιστης + %1$s συλλεχθηκαν %2$d τραγουδιστες + + + %1$s σχολίασαν + + + %1$s το άκουσατε μαζί με έναν φίλο + %1$s το ακουσατε μαζι με %2$d φίλους + + + + %1$s δημιουργηθηκε μια λίστα αναπαραγωγής + %1$s δημιουργηθηκαν %2$d λίστες αναπαραγωγής + + + %1$s απο %2$s + πριν από λίγα δευτερόλεπτα + + πριν από ένα λεπτό + %1$d λεπτά πριν + + + πριν από μία ώρα + %1$d ωρες πριν + + + μια ημέρα πριν + %1$d ημερες πριν + + %1$s\'s πρόσφατα παιχθηκε + %1$s\'s Αγαπημένα + %1$s\'s Λίστα αναπαραγωγής + Μουσική + Βιογραφία + + Παρόμοια + + Τραγούδια + + + %1$d Τραγούδι + %1$d Τραγούδια + + Δραστηριότητα + + Ακολουθοι + + Ακολουθουν + + Ακολουθοι: %1$d, Ακολουθουν: %2$d + Συνδεθείτε στο + Αποσυνδεθείτε από + Θέλετε να αποθηκεύσετε αυτόματα τα δεδομένα σχετικά με όλα τα κομμάτια που παίζουν στο Hatchet; + Θα το κάνουμε αυτό για σας! Παρακαλούμε ενεργοποιήστε την υπηρεσία μας από τις ρυθμίσεις του συστήματος σας. + Κλείσιμο + Προβολή άλμπουμ + Σύνδεση + Επιλέξτε τις υπηρεσίες που θέλετε να χρησιμοποιήσετε. Το Tomahawk λειτουργεί καλύτερα όταν ενεργοποιούνται περισσότερες υπηρεσίες. + Σύνθεση + Πληροφορίες + Μπορείτε να δείτε μέσα από ένα τραγουδι με ένα άγγιγμα πατωντας το κουμπί αναπαραγωγης + Πατήστε για να προσθέσετε το κομμάτι στην σειρά αναμονής. + Τραβήξτε προς τα πάνω για να δείτε την τρέχουσα λίστα αναπαραγωγής + Τραβήξτε προς τα κάτω για να κλείσετε το συρτάρι αναπαραγωγής + Σύρετε προς τα αριστερά/δεξιά για να μεταβείτε στο επόμενο/προηγούμενο κομμάτι. Μακρύ-πατήμα για να ανοίξετε το μενού περιεχομένου. Διπλό χτύπημα για να προσθέσετε στα αγαπημένα. + + Επόμενο + Πρώτα βήματα + Η μουσική είναι παντού.\Τώρα δεν χρειάζεται να είναι. + Σταματήστε κυνηγώντας τη μουσική σε όλο τον ιστό - για τις υπηρεσίες που δεν χρησιμοποιείτε, ή πηγές που δεν έχουν πρόσβαση. Λαμβάνοντας υπόψη το όνομα ή τη σύνδεση με ένα τραγούδι, άλμπουμ ή λίστα αναπαραγωγής, Tomahawk θα βρει την καλύτερη διαθέσιμη πηγή για εσάς και απλά να παίξουν. + Θέλετε να το κοινωνικοποιησετε; + Με το Hatchet μπορείτε να έχετε τελικά την μουσική συλλογή σας σε ένα μέρος. Μοιραστείτε τις λίστες αναπαραγωγής και τα αγαπημένα σας κομμάτια. Ή απλά συγχρονίστε την εφαρμογή με το Tomahawk στη μηχανή επιφάνεια εργασίας σας. + Σύνδεση + Έγινε! + Όλα τελείωσαν! + Διασκεδάστε εξερευνώντας την εφαρμογή και να είστε βέβαιος να μας πείτε τη γνώμη σας! + + Επιλέξτε μια λίστα αναπαραγωγής για να προσθέσετε το %1$d τραγουδι: + Επιλέξτε μια λίστα αναπαραγωγής για να προσθέσετε τα %1$d τραγουδια: + + + + + diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 000000000..a7ae5752e --- /dev/null +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,261 @@ + + + OK + Cancel + Login + Logout + Register + Register & Login + Rescan now + Internal Storage + Tracks + Albums + Artists + Users + Top Hits + Top Albums + Albums and EPs + Other releases + Playlist Details + Album Details + Search + Playlist + Shuffle is on + Shuffle is off + Repeat is on + Repeat is off + unknown + no track + Save the current playlist + Create playlist + + Title + Create station + Search for artists/songs/genres + Genres + Moods + E-Mail/Username + Username + Password + Password confirmation + E-Mail (optional) + This field is required + Passwords don\'t match + Username or password incorrect. + Account rejected by server. + Unable to authenticate. Please check your connection. + Action not allowed, account is in use elsewhere. + Your account has expired. + + Logged into %1$s + + Logged out of %1$s + Local Collection + Local + All + Choose the directories which should be scanned for local media. + Sync your data with %1$s. + Playback + Equaliser + Customise the audio output. + Scrobble Everything + Scrobble every track from every music player app on the phone to %1$s. + Plug In to Play + Starts playing music as soon as a headset is connected. + Preferred Audio Quality + The preferred audio quality if not connected to WiFi. + Info + App-Version + Feedback + Share your ideas or ask for support. + Rate the App + Rate Tomahawk on the Google Play Store. + Visit our Website + Open tomahawk-player.org in your browser. + Send Log + Send your log file for debug purposes. + Please briefly describe the issue you are having and provide an email so we can contact you if needed. Thanks! + Your email address + Issue + Tomahawk has crashed + An unexpected error occurred forcing the application to stop. Please help us fix this by sending us error data, all you have to do is click OK. + You might add your comments about the problem below: + Thank you ! + Play + Add to Queue + Add to Playlist + Create Station + Delete + Remove from Playlist + + Remove from Collection + + Add to Collection + + Love + + Unlove + Share + You have to log into Hatchet to be able to share this playlist. + Feed + Charts + Collection + Playlists + Stations + Favourites + Settings + Cloud Collections + Recently Added + Artist A-Z + Play Count + Album A-Z + A-Z + History + Normal + Good + Best + Open Navigation Drawer + Close Navigation Drawer + + Following + + Follow + + Suggested people to follow + + %1$s is now following + + + %1$s loved a track + %1$s loved %2$d tracks + + + + %1$s collected a track + %1$s collected %2$d tracks + + + + %1$s collected an album + %1$s collected %2$d albums + + + + %1$s collected an artist + %1$s collected %2$d artists + + + %1$s commented on + + + %1$s listened along to a friend + %1$s listened along to %2$d friends + + + + %1$s created a playlist + %1$s created %2$d playlists + + + %1$s by %2$s + a few seconds ago + + a minute ago + %1$d minutes ago + + + an hour ago + %1$d hours ago + + + a day ago + %1$d days ago + + %1$s\'s recently played + %1$s\'s Favourites + %1$s\'s Playlist + My recently played + My Favourites + My Playlist + Music + Biography + + Similar + + Songs + + + %1$d Song + %1$d Songs + + Activity + + Followers + + Following + + Followers: %1$d, Following: %2$d + Enable + Disable + Log into + Log out of + Download Plugin for + Want to automatically save data about all the tracks you play on Hatchet? + We\'ll do that for you! Please enable our service in your system settings. + Close + View Album + Connect + Select the services you want to use. Tomahawk works better when more services are activated. + Manually installed + Advanced + Info + You can seek through a track with a long-press on the Play button. + Tap to add the track to the queue. + Pull up to see the current playlist. + Pull down to close the playback drawer. + Swipe left/right to skip to the next/previous track. Long-press to open context menu. Double-tap to add to favourites. + + Next + Getting started + Music is everywhere.\nNow you don\'t have to be. + Stop chasing music across the web - to services you don\'t use, or sources you don\'t have access to. Given the name or link to a song, album or playlist, Tomahawk will find the best available source for you and just play it. + Want to socialise? + With Hatchet you can finally have your music collection in one place. Share your playlists and favourite tracks. Or simply sync the App with Tomahawk on your desktop machine. + Connect + Done! + All done! + Have fun exploring the app and be sure to tell us what you think! + + Choose a playlist to add %1$d track to: + Choose a playlist to add %1$d tracks to: + + Sorry, this resolver doesn\'t support seeking. + Plugin installation + You are about to install a Tomahawk plugin from an unknown source. This can be a security risk. + Do you really want to install this Plugin? + Uninstalling plugin + Do you really want to uninstall this Plugin? + Choose an account: + Can\'t open file. + Unknown album + Unknown artist + Couldn\'t find Settings App on your device. Please give Tomahawk access to your notifications in your System Settings. + Warning: You are about to download a plugin that contains closed-source parts. + The required plugin application is outdated and no longer compatible. Do you want to update it now? + The required plugin application is no longer installed. Do you want to install it now? + + new + Pause + Play + Previous Track + Next Track + Add to Favourites + Remove from Favourites + + Now Playing + + Shuffle & Play + Loading station: + Suggested Stations + My Stations + diff --git a/app/src/main/res/values-eo-rXX/strings.xml b/app/src/main/res/values-eo-rXX/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-eo-rXX/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-es-rPE/strings.xml b/app/src/main/res/values-es-rPE/strings.xml new file mode 100644 index 000000000..ed4cdc4bc --- /dev/null +++ b/app/src/main/res/values-es-rPE/strings.xml @@ -0,0 +1,85 @@ + + + Ok + Cancelar + Iniciar Sesión + Salir Sesión + Registrarse + Almacenamiento Interno + Albumes + Aristas + Usuarios + Top Canciones + Top Álbumes + Álbumes y EP\'s + Otros lanzamientos + Detalles de la Playlist + Detalles del Álbum + Búsqueda + Repetir está ON + Repetir está Apagado + Desconocido + No Música + Guardar esta Playlist + Crear una Playlist + + Título + Crear estación + Buscar por artistas/canciones/géneros + Géneros + Estados de Ánimo + E-Mail/Usuario + Usuario + Contraseña + Confirmar contraseña + E-Mail (opcional) + Se requiere rellenar este espacio + Las contraseñas no coinciden + El usuario o contraseña es incorrecto + Cuenta rechazada por el servidor + No se puede autenticar. Porfavor revise su conexión + Accion no permitida, cuenta está siendo usada en otro lugar. + Tu cuenta ha expirado. + + Logeado en %1$s + + Desconectado de %1$s + Colección Local + Local + Todo + Ecualizador + Conectar para Escuchar + Calidad de audio preferida + Información + Versión de la Aplicación + Opiniones + Puntuar la App + Visitar nuestro sitio web + Enviar Registros + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml new file mode 100644 index 000000000..4096e06f8 --- /dev/null +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -0,0 +1,232 @@ + + + OK + Cancelar + Iniciar sesión + Cerrar Sesión + Regístrese + Registrarse e iniciar sesión + Volver a buscar + Almacenamiento Interno + Pistas + álbumes + Artistas + Usuarios + + Top hits + Mejores álbumes + Álbumes y EPs + Otros lanzamientos + Detalles de la playlist + Detalles del álbum + Busqueda + Playlist + Modo aleatorio activado + Modo aleatorio desactivado + Repeción activada + Repetición desactivada + desconocido + Ninguna pista + Guardar esta playlist + Crear Playlist + + Titulo + Correo/nombre de usuario + Nombre de usuario + Contraseña + Confirme su contraseña + Correo (opcional) + El campo es requerido + Las contraseñas no coinciden + Nombre de usuario o contraseña incorrecta + Cuenta rechazada por el servidor + Imposible autenticar. por favor revisa tu conexión + Acción no permitida, la cuenta está siendo utilizada + Tu cuenta ha expirado + + Iniciada sesión en %1$s + + Cerrada la sesión de %1$s + Colección Local + Local + Todo + Escoje la ubicación en la cual deberíamos buscar tú música + Sincronizando tus datos al %1$s. + Reproducción + Ecualizador + Personaliza la salida de audio + Hacer scrobblings de todo. + Haz scrobble de cada pista en cada reproductor en el teléfono a %1$s. + Conecte para reproducir + Reproducir música tan pronto se conecten los auriculares. + Calidad de audio preferida + La calidad preferida de audio si no esta conectado el WiFi. + Info + App-Version + retroalimentación + Comparte tus ideas o pide ayuda. + Enviar Log + Envía tu log de errores con propósitos de depuración + Por favor describa brevemente el inconveniente que estás teniendo e indicanos una dirección de correo donde podamos contactarte en caso de ser necesario. Gracias. + Tu dirección de correo + Problema + Lo sentimos tomahawk se detuvo. + Un error inesperado ha ocurrido y la aplicación se detuvo . Por favor ayudanos enviando la información del error para repararlo, lo único que tienes que hacer es un click en OK. + Aquí puedes enviar tus comentarios sobre el problema: + ¡Gracias! + Reproducir + Añadir a la cola + Agregar al playlist + Eliminar + Remover de la Playlist + + Remover de la coleccion + + Agregar a la colección + + Me encanta + + Ya no me gusta + Compartir + Debes ingresar a Hatchet para poder compartir esta lista de reproducción. + Actividad Reciente + Colección + Playlists + Favoritas + Ajustes + Conexiones en la nube + Añadidas recientemente + Artistas A-Z + Conteo de reproducción + A + A-Z + Historial + Normal + Buena + La mejor + Abrir el navegador + Cerrar el navegador + + Siguiendo + + Seguir + + Sugerencias para seguir + + %1$s esta siguiendo a + + + %1$s le gustó una canción + %1$s le gustaron %2$d canciones + + + + %1$s agregó una canción + %1$s agregó %2$d canciones + + + + %1$s agregó un álbum + %1$s agregó %2$d álbumes + + + + %1$s agregó un artista + %1$s agregó %2$d artistas + + + %1$s comentó en + + + %1$s escucho junto a un amigo + %1$s escuchó junto a %2$d amigos + + + + %1$s creó una lista de reproducción + %1$s creo %2$d listas + + + %1$s por %2$s + hace unos segundos + + hace un minuto + hace %1$d minutos + + + hace una hora + hace %1$d horas + + + hace un día + hace %1$d días + + %1$s escuchó recientemente + Favoritas de %1$s + Listas de %1$s + Mis reproducciones recientes + Mis favoritas + Mi lista de reproducción + Música + Biografía + + Similar + + Canciones + + + %1$d Canción + %1$d Canciones + + Actividad + + Seguidores + + Siguiendo + + Seguidores: %1$d, Siguiendo: %2$d + Iniciar sesión en + Cerrar sesión en + Descargar Plugin para + ¿Quieres guardar automáticamente la información de las pistas que reproduces en Hatchet? + ¡Nosotros lo haremos por ti! Por favor habilita nuestro servicio en la configuración de tu dispositivo + Cerrar + Ver Álbum + Conectar + Selecciona los serivicios que quieres usar, Tomahawk funciona mejor cuando más servicios estan activos. + Instalada manualmente + Avanzado + Información + Puedes buscar dentro de una canción manteniendo presionado el boton Reproducir. + Presione para añadir la canción a la cola + Deslice hacia arriba para ver la lista de reproducción actual. + Deslice hacia abajo para cerrar el panel de reproducción. + Deslice hacia la derecha/izquierda para saltar a la canción siguiente/anterior. Mantenga presionado para abrir el menu contextual. Presione dos veces para agregar a favoritos. + + Siguiente + Primeros pasos + La música esta en todos lados.\nAhora tu no tienes que estarlo. + Deje de perseguir la música a través de la web - a servicios que no usas, o fuentes que no tienes acceso. Dado el nombre o el enlace a una canción, álbum o lista de reproducción, Tomahawk encontrará la mejor fuente disponible para ti. + ¿Quieres socializar? + Con Hatchet finalmente puedes tener tu colección de música en un solo sitio. Comparte tus listas y canciones favoritas. O simplemente sincroniza la aplicación con Tomahawk en tu equipo de escritorio. + Conectar + ¡Listo! + ¡Todo listo! + ¡Diviertete explorando la aplicación y dinos lo que piensas! + + Escoge una lista de reproducción para agregarle %1$d canciones: + Escoge una lista de reproducción para agregarle %1$d canciones: + + Instalación de plugin + Vas a instalar un plugin de Tomahawk desde una fuente desconocida. Esto puede ser un riesgo de seguridad. + ¿Está seguro que desea instalar este Plugin? + Desinstalando plugin + ¿Está seguro que desea desinstalar este Plugin? + Escoja una cuenta: + No se puede abrir el archivo. + Álbum desconocido + Artista desconocido + + + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..637059297 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,259 @@ + + + Aceptar + Cancelar + Acceder + Salir + Registrarse + Registrarse y acceder + Reanalizar ahora + Almacenaje interno + Canciones + Álbumes + Artistas + Usuarios + Grandes éxitos + Mejores álbumes + Álbumes y EP + Otras publicaciones + Detalles de la lista + Detalles del álbum + Buscar + Lista de reproducción + Modo aleatorio activado + Modo aleatorio desactivado + Repetición activada + Repetición desactivada + desconocido + no hay pistas + Guardar la lista actual + Crear lista de reproducción + + Título + Crear estación + Búsqueda de artistas/canciones/géneros + Géneros + Usuario o correo electrónico + Nombre de usuario + Contraseña + Confirmar la contraseña + Correo electrónico (opcional) + Este campo es obligatorio + Las contraseñas no coinciden + El nombre de usuario o la contraseña son incorrectos. + El servidor rechazó la cuenta. + Falló la autenticación. Compruebe la conexión. + Acción no permitida: la cuenta se está utilizando en otro sitio. + La cuenta ha caducado. + + Sesión iniciada como %1$s + + Se cerró la sesión de %1$s + Colección local + Local + Toda + Elija las carpetas que se explorarán en busca de multimedia local. + Sincronice sus datos con %1$s + Reproducción + Ecualizador + Personalice la salida de audio. + Registrar todo en Last.fm + Hacer «scrobbling» de cada pista en todos los reproductores del teléfono en %1$s. + Conectar para reproducir + Comienza a reproducir la música en cuanto se conectan los auriculares. + Calidad de sonido preferida + La calidad de sonido preferida cuando no hay conexión con una red wifi. + Información + Versión de la aplicación + Comentarios + Comparta ideas o pida ayuda + Califica la aplicación + Califica de Tomahawk en la tienda de Google Play. + Visita nuestro sitio web + Abra tomahawk-player.org en su navegador. + Enviar registros + Envíe su archivo de registro para ayudar a corregir errores. + Describa brevemente el problema que ha experimentado y proporcione una dirección de correo electrónico para que podamos contactar con usted si es necesario. ¡Gracias! + Dirección de correo electrónico + Problema + Tomahawk se cerró inesperadamente + Se produjo un error inesperado que provocó el cierre de la aplicación. Para ayudarnos a corregirlo, envíenos los datos del error: todo lo que tiene que hacer es pulsar en Aceptar. + Puede añadir sus comentarios sobre el problema a continuación (en inglés): + ¡Gracias! + Reproducir + Añadir a la cola + Añadir a la lista + Crear Estación + Eliminar + Quitar de la lista + + Quitar de la colección + + Añadir a la colección + + Me gusta + + Ya no me gusta + Compartir + Debes ingresar a Hatchet para poder compartir esta lista de reproducción. + Noticias + Gráficas + Colección + Listas de reproducción + Estaciones + Favoritos + Configuración + Colecciones en línea + Añadidos recientemente + Artistas A-Z + N.º de reproducciones + Álbumes A-Z + A-Z + Historial + Normal + Buena + La mejor + Abrir panel de navegación + Cerrar panel de navegación + + Siguiendo + + Seguir + + Quizá quiera seguir a + + %1$s ahora sigue a + + + A %1$s le gustó una canción + A %1$s le gustaron %2$d canciones + + + + %1$s agregó una canción + %1$s agregó %2$d canciones + + + + %1$s agregó un álbum + %1$s agregó %2$d álbumes + + + + %1$s agregó un artista + %1$s agregó %2$d artistas + + + %1$s comentó en + + + %1$s escuchó junto a un amigo + %1$s escuchó junto a %2$d amigos + + + + %1$s creo una lista de reproducción + %1$s creó %2$d listas de reproducción + + + %1$s de %2$s + hace unos segundos + + hace un minuto + hace %1$d minutos + + + hace una hora + hace %1$d horas + + + hace un día + hace %1$d días + + %1$s escuchó recientemente + Favoritas de %1$s + Lista de %1$s + Mis reproducciones recientes + Mis Favoritos + Mis Lista de Reproducción + Música + Biografía + + Similares + + Canciones + + + %1$d canción + %1$d canciones + + Actividad + + Seguidores + + Siguiendo + + Seguidores: %1$d, Siguiendo: %2$d + Habilitado + Deshabilitado + Acceder a + Salir de + Descargar complemento para + ¿Quiere guardar automáticamente información de pistas reproducidas en Hatchet? + Lo haremos por usted. Active nuestro servicio en la configuración del sistema. + Cerrar + Ver álbum + Conectar + Seleccione los servicios que quiera utilizar. Tomahawk funciona mejor cuando activa más servicios. + Instalado manualmente + Avanzado + Información + Para avanzar en una pista, mantenga pulsado el botón Reproducir. + Pulse para añadir la pista a la cola. + Deslice hacia arriba para ver la lista actual. + Deslice hacia abajo para cerrar la cola de reproducción. + Deslice a la izquierda o a la derecha para cambiar a la canción anterior o la siguiente. Pulse prolongadamente para abrir el menú contextual. Toque dos veces para añadir a los favoritos. + + Siguiente + Primeros pasos + La música está en todas partes.\nNo tiene por qué perseguirla. + Ahora no es necesario que se registre en innumerables servicios para acceder a la música que le gusta. Tomahawk encontrará la mejor fuente musical para la canción, el álbum o la lista de reproducción que proporcione, mediante su nombre o un enlace. + ¿Quiere socializar? + Con Hatchet puede organizar su colección musical en un solo sitio. Comparta sus listas de reproducción y canciones preferidas, o bien, úselo para sincronizar la apli con Tomahawk en su equipo de escritorio. + Conectar + Hecho. + Todo listo. + Que disfrute la aplicación. No dude en compartir su opinión con nosotros. + + Escoge una lista de reproducción para agregarle %1$d cancion: + Escoge una lista de reproducción para agregarle %1$d canciones: + + Este servicio no permite cambiar de posición en la duración de la pista. + Instalación de complemento + Está a punto de instalar un complemento de Tomahawk que proviene de un origen desconocido. Esto puede representar un riesgo a la seguridad. + ¿Confirma que quiere instalar este complemento? + Desinstalando extensión + ¿Confirma que quiere desinstalar este complemento? + Elija una cuenta: + No se puede abrir el archivo. + Álbum desconocido + Artista desconocido + No se pudo encontrar la configuración de la aplicación en su dispositivo. Por favor dé a Tomahawk acceso a sus alertas en tu Configuración del sistema. + Advertencia: está a punto de descargar un plugin que contiene partes de código cerrado. + La aplicación plug-in requerida está obsoleta y ya no es compatible. ¿Quieres actualizar ahora? + La aplicación plug-in requerida no se encuentra instalada. ¿Quieres instalarla ahora? + + nueva + Pausar + Reproducir + Pista anterior + Siguiente pista + Añadir a favoritos + Remover de favoritos + + Reproduciendo ahora + + Cargando estación: + Estaciones sugeridas + Mis estaciones + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-et/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-eu-rES/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml new file mode 100644 index 000000000..4a4479a7c --- /dev/null +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -0,0 +1,152 @@ + + + بسیار خوب + انصراف + ورود + خروج از برنامه + ثبت نام + ثبت نام & ورود + اسکن مجدد + حافظه داخلی + آهنگ ها + آلبوم ها + خواننده ها + کاربران + بیشترین بازدیدها + آلبوم های ویژه + محتوای لیست پخش + محتوای آلبوم + جستجو + لیست پخش + تصادفی روشن است + تصادفی خاموش است + تکرار روشن است + تکرار خاموش است + ناشناخته + بدون آهنگ + ذخیره کردن لیست پخش جاری + ساختن لیست پخش + + عنوان + ساختن ایستگاه + جستجو برای خواننده/آهنگ ها/دسته ها + دسته ها + حالت ها + ایمیل/نام کاربری + نام کاربری + رمز عبور + تکرار رمز عبور + ایمیل (اختیاری) + این فیلد اجباری می باشد + رمز عبورها یکسان نمی باشند + نام کاربری و یا رمز عبور اشتباه می باشند + حساب کاربری توسط سرور رد شد. + اعتبار سنجی ممکن نمی باشد.لطفا ارتباط خود را چک کنید. + این عمل امکان پذیر نمی باشد، این حساب در جای دیگری در حال استفاده می باشد. + حساب شما منقضی شده است. + + به %1$s وارد شدید + + از %1$s خارج شدید + مجموعه محلی + محلی + همه + برای اسکن رسانه های محلی باید دایرکتوری را انتخاب نمایید + همگام سازی اطلاعات شما با %1$s + پخش + اکولایزر + سفارشی کردن صدای خروجی + تغییر دادن همه چیز + تغییر دادن هر آهنگ برای هربرنامه پخش کنند موزیک در گوشی به %1$s. + اتصال برای پخش + اطلاعات + نسخه برنامه + بازخورد + امتیاز دادن به برنامه + از وبسایت ما دیدن کنید + ارسال گزارش + مساله + متشکرم ! + اجرا + اضافه کردن به صف + اضافه کردن به لیست پخش + حذف + حذف کردن از لیست پخش + + حذف کردن از مجموعه + + افزودن به مجموعه + + مورد علاقه + + حذف از مورد علاقه + به اشتراک گزاری + نمودارها + مجموعه + لیست های پخش + علاقمندی ها + تنظیمات + تعداد اجرا + آلبوم A-Z + A-Z + تاریخچه + نرمال + خوب + بهترین + باز کردن منو + بستن منو + + در حال پیروی کردن + + پیروی کردن + + پیشنهاد کردن به دیگران برای پیروی کردن + + + + + + + + + + چند ثانیه قبل + لیست پخش من + موزیک + بیوگرافی + + شبیه + + آهنگ + + فعالیت + + فالورها + + در حال فالو کردن + + ورود به + خروج از + دانلود کردن پلاگین برای + بستن + مشاهده آلبوم + اتصال + پیشرفته + اطلاعات + + بعدی + شروع شدن + اتصال + اتمام! + همه چیز تمام شد! + نصب کردن پلاگین + حذف کردن پلاگین + انتخاب کردن یک حساب کاربری + امکان باز کردن فایل وجود ندارد. + البوم ناشناخته + خواننده ناشناخته + + جدید + + + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..a16bf5359 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,261 @@ + + + OK + Peruuta + Kirjaudu + Kirjaudu ulos + Rekisteröidy + Rekisteröidy & kirjaudu + Etsi nyt + Sisäinen levy + Kappaleet + Albumit + Artistit + Käyttäjät + Parhaat hitit + Parhaat albumit + Albumit ja EPt + Muut julkaisut + Soittolistan tiedot + Albumin tiedot + Hae + Soittolista + Sekoitus käytössä + Sekoitus ei käytössä + Toisto käytössä + Toisto ei käytössä + tuntematon + ei kappaletta + Tallenna nykyinen soittolista + Luo soittolista + + Nimi + Luo asema + Etsi artisteja/kappaleita/lajityyppejä + Lajityypit + Mielialat + Sähköposti/käyttäjätunnus + Käyttäjätunnus + Salasana + Salasanan vahvistus + Sähköposti (valinnainen) + Tämä kenttä on pakollinen + Salasanat eivät täsmää + Käyttäjätunnus tai salasana virheellinen. + Palvelin hylkäsi tilin. + Tunnistautuminen epäonnistui. Tarkista yhteytesi. + Toimintoa ei sallita; tili on käytössä muualla. + Tilisi on vanhentunut. + + Kirjauduttu palveluun %1$s + + Kirjauduttu ulos palvelusta %1$s + Paikallinen kokoelma + Paikallinen + Kaikki + Valitse kansiot, joista etsitään paikallista mediaa. + Synkronoi tietosi palvelun %1$s kanssa. + Soitto + Taajuuskorjain + Mukauta äänen ulostuloa. + Skrobblaa kaikki + Skrobblaa kaikki puhelimen millä tahansa musiikkisoittimella kuunnellut kappaleet palveluun %1$s. + Aloita soitto kytkemällä + Alkaa soittaa musiikkia heti, kun kuulokkeet on kytketty. + Haluttu äänenlaatu + Haluttu äänenlaatu, jos ei olla WiFi-yhteydessä. + Info + Sovelluksen versio + Palaute + Kerro ideasi tai pyydä tukea + Arvostele sovellus + Arvostele Tomahawk Google Play-kaupassa. + Avaa verkkosivumme + Avaa tomahawk-player.org selaimeesi. + Lähetä loki + Lähetä lokitiedosto vianjäljitystä varten. + Kerro kokemasi ongelma lyhyesti ja anna sähköpostiosoite, jotta voimme tarvittaessa ottaa sinuun yhteyttä. Kiitos! + Sähköpostiosoitteesi + Ongelma + Tomahawk on kaatunut + Odottamaton virhe tapahtui, kun sovellusta pakotettiin pysähtymään. Auta meitä korjaamaan ongelma lähettämällä virhetiedot. Sinun tarvitsee vain painaa OK. + Alle voit kirjoittaa lisätietoja ongelmasta: + Kiitos! + Soita + Lisää jonoon + Lisää soittolistaan + Luo asema + Poista + Poista soittolistalta + + Poista kokoelmasta + + Lisää kokoelmaan + + Tykkää + + Lakkaa tykkäämästä + Jaa + Sinun täytyy kirjautua Hatchetiin, jotta voit jakaa tämän soittolistan. + Syöte + Listat + Kokoelma + Soittolistat + Asemat + Suosikit + Asetukset + Pilvikokoelmat + Viimeksi lisätyt + Artisti A–Ö + Soittomäärä + Albumi A–Ö + A–Ö + Historia + Tavallinen + Hyvä + Paras + Avaa vetovalikko + Sulje vetovalikko + + Seuraamassa + + Seuraa + + Ehdotuksia ihmisistä, joita seurata + + %1$s seuraa nyt + + + %1$s tykkäsi kappaleesta + %1$s tykkäsi %2$d kappaleesta + + + + %1$s keräsi kappaleen + %1$s keräsi %2$d kappaletta + + + + %1$s keräsi albumin + %1$s keräsi %2$d albumia + + + + %1$s keräsi artistin + %1$s keräsi %2$d artistia + + + %1$s kommentoi + + + %1$s kuunteli kaverin kanssa + %1$s kuunteli %2$d kaverin kanssa + + + + %1$s loi soittolistan + %1$s loi %2$d soittolistaa + + + %1$s artistilta %2$s + muutama sekunti sitten + + minuutti sitten + %1$d minuuttia sitten + + + tunti sitten + %1$d tuntia sitten + + + päivä sitten + %1$d päivää sitten + + Käyttäjän %1$s viimeksi kuuntelemat + Käyttäjän %1$s suosikit + Käyttäjän %1$s soittolista + Viimeksi kuuntelemani + Suosikkini + Soittolistani + Musiikki + Biografia + + Samankaltaiset + + Kappaleet + + + %1$d kappale + %1$d kappaletta + + Toiminta + + Seuraajat + + Seuraamassa + + Seuraajia: %1$d, Seuraa: %2$d + Ota käyttöön + Poista käytöstä + Kirjaudu\npalveluun + Kirjaudu ulos\npalvelusta + Lataa lisäosa\npalvelulle + Haluatko tallentaa kaikkien kuuntelemiesi kappaleiden tiedot automaattisesti Hatchetiin? + Teemme sen puolestasi! Ota palvelumme käyttöön järjestelmäasetuksista. + Sulje + Näytä albumi + Yhdistä + Valitse palvelut, joita haluat käyttää. Tomahawk toimii sitä paremmin, mitä enemmän palveluita on käytössä. + Asennettu manuaalisesti + Lisäasetukset + Info + Voit kelata kappaletta pitämällä soittopainiketta pohjassa pitkään. + Lisää kappale jonoon napauttamalla. + Vedä ylös nähdäksesi nykyisen soittolistan. + Vedä alas sulkeaksesi toiston vetovalikon. + Siirry seuraavaan/edelliseen kappaleeseen pyyhkäisemällä vasemmalle/oikealle. Avaa kontekstivalikko pitämällä pitkään pohjassa. Lisää suosikkeihin kaksoisnapauttamalla. + + Seuraava + Aloittaminen + Musiikki on kaikkialla.\nNyt sinun ei tarvitse olla. + Lakkaa jahtaamasta musiikkia ympäri verkkoa – palveluihin, joita et käytä, tai lähteisiin, joihin et pääse. Kun Tomahawkille antaa kappaleen, albumin tai soittolistan nimen tai linkin, Tomahawk etsii parhaan käytettävissäsi olevan lähteen ja alkaa soittaa kappaletta sieltä. + Haluatko jakaa? + Hatchetin avulla voit vihdoin säilyttää koko musiikkikokoelmasi yhdessä paikassa. Voit jakaa soittolistojasi ja suosikkikappaleitsi. Voit myös synkronoida tämän sovelluksen työpöytäkoneesi Tomahawkin kanssa. + Yhdistä + Valmista! + Kaikki on valmista! + Pidä hauskaa tutkiessasi sovellusta ja kerro meille, mitä mieltä olet siitä! + + Valitse soittolista, johon %1$d kappale lisätään: + Valitse soittolista, johon %1$d kappaletta lisätään: + + Tämä selvitin ei tue kelaamista. + Liitännäisen asennus + Olet asentamassa Tomahawk-liitännäistä tuntemattomasta lähteestä. Tämä voi olla tietoturvariski. + Haluatko varmasti asentaa tämän liitännäisen? + Liitännäisen poisto + Haluatko varmasti poistaa tämän liitännäisen? + Valitse tili: + Tiedostoa ei voida avata. + Tuntematon albumi + Tuntematon artisti + Laitteeltasi ei löydetty Asetukset-sovellusta. Ole hyvä ja anna järjestelmäasetuksista Tomahawkille pääsy ilmoituksiisi. + Varoitus: olet lataamassa liitännäistä, jossa on suljetun lähdekoodin osia. + Vaadittu liitännäissovellus on vanhentunut eikä ole enää yhteensopiva. Haluatko päivittää sen nyt? + Vaadittu liitännäissovellus ei ole enää asennettuna. Haluatko asentaa sen nyt? + + uusi + Tauko + Soita + Edellinen kappale + Seuraava kappale + Lisää suosikkeihin + Poista suosikeista + + Nyt soi + + Sekoita & soita + Ladataan asemaa: + Ehdotetut asemat + Asemani + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..46a5600d0 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,261 @@ + + + OK + Annuler + Connexion + Déconnexion + S\'inscrire + S\'inscrire & se connecter + Rescanner + Stockage interne + Pistes + Albums + Artistes + Utilisateurs + Top Hits + Top Albums + Albums et EP + Autres formats + Détails de la playlist + Détails de l\'album + Rechercher + Playlist + Lecture aléatoire activée + Lecture aléatoire désactivée + Répétition activée + Répétition désactivée + inconnu + Aucune piste + Sauvegarder la playlist en cours + Créer une playlist + + Titre + Créer une radio + Rechercher par artistes/chansons/genres + Genres + Ambiances + E-mail/Nom d\'utilisateur + Nom d\'utilisateur + Mot de passe + Confirmation du mot de passe + E-mail (facultatif) + Ce champ est obligatoire + Les mots de passe ne correspondent pas + Nom d\'utilisateur ou mot de passe incorrect + Compte rejeté par le serveur + Authentification impossible. Vérifiez votre connexion. + Cette action n\'est pas autorisée, ce compte est déjà utilisé ailleurs. + Votre compte a expiré. + + Connecté à %1$s + + Déconnecté de %1$s + Collection locale + Local + Tout + Choisissez les répertoires locaux à scanner. + Synchroniser vos données avec %1$s. + Diffusion + Égaliseur + Ajuster le rendu audio. + Tout scrobbler + Scrobbler toutes les pistes de toutes les application musicales du téléphone vers %1$s. + Brancher et écouter + Lancer la lecture de musique dès qu\'un casque audio est branché. + Qualité audio préférée + Qualité audio préférée hors connexion WiFi. + Info + Version de l\'application + Retours + Donnez-nous vos idées ou demandez de l\'aide. + Noter l\'application + Noter Tomahawk sur le Google Play Store. + Visitez notre site web + Ouvrir tomahawk-player.org dans votre navigateur. + Envoyer les logs + Envoyer vos logs pour aider à corriger le problème. + Décrivez brièvement le problème que vous rencontrez et donnez une adresse e-mail afin que nous puissions vous contacter si besoin. Merci ! + Votre adresse e-mail + Problème + Tomahawk a planté + Une erreur inattendue a entraîné l\'arrêt de l\'application. Aidez-nous à corriger le problème en nous envoyant un rapport d\'erreur, pour ce faire il vous suffit de cliquer sur OK. + Vous pouvez ajouter vos commentaire concernant le problème ici : + Merci ! + Jouer + Ajouter à la file + Ajouter à la playlist + Créer une radio + Supprimer + Retirer de la playlist + + Supprimer de la collection + + Ajouter à la collection + + Favoris + + Supprimer des favoris + Partager + Vous devez vous connecter à Hatchet pour partager cette playlist. + Flux + Charts + Collection + Playlists + Radios + Favoris + Paramètres + Collections du cloud + Ajouté récemment + Artiste A-Z + Nombre de lectures + Album A-Z + A-Z + Historique + Normal + Bon + Meilleur + Ouvrir le panneau de navigation + Fermer le panneau de navigation + + Abonné + + S\'abonner + + Suggestion d\'utilisateurs à suivre + + %1$s suit maintenant + + + %1$s a ajouté une piste à ses favoris + %1$s a ajouté %2$d pistes à ses favoris + + + + %1$s a ajouté une piste à sa collection + %1$s a ajouté %2$d pistes à sa collection + + + + %1$s a ajouté un album à sa collection + %1$s a ajouté %2$d albums à sa collection + + + + %1$s a ajouté un artiste à sa collection + %1$s a ajouté %2$d artistes à sa collection + + + %1$s a ajouté un commentaire sur + + + %1$s a écouté ce qu\'écoutait un·e de ses ami·e + %1$s a écouté ce qu\'écoutaient %2$d de ses ami·es + + + + %1$s a créé une playlists + %1$s a créé %2$d playlists + + + %1$s de %2$s + il y a quelques secondes + + il y a une minute + il y a %1$d minutes + + + il y a une heure + il y a %1$d heures + + + hier + il y a %1$d jours + + %1$s\'s a récemment écouté + Favoris de %1$s + Listes de lecture de %1$s + Mes titres récemment joués + Mes favoris + Ma playlist + Musique + Biographie + + Similaire + + Chansons + + + %1$d chanson + %1$d chansons + + Activité + + Abonnements + + Abonnés + + Abonnés : %1$d, Abonnements : %2$d + Activer + Désactiver + Se connecter à + Se déconnecter de + Télécharger l\'extension pour + Vous souhaitez sauvegarder automatiquement des infos sur tous les morceaux que vous jouez sur Hatchet ? + On peut faire ça ! Il vous suffit d\'activer ce service dans vos paramétres d\'utilisation. + Fermer + Voir l\'album + Se connecter + Sélectionnez les services que vous souhaitez utiliser. Tomahawk fonctionne d\'autant mieux que plusieurs services sont activés. + Installé manuellement + Avancé + Info + Vous pouvez naviguer dans un morceau en restant pressé sur le bouton de lecture. + Pressez pour ajouter le morceau à la file. + Déplacez vers le haut pour voir la playlist en cours. + Déplacez vers le bas pour fermer le panneau de lecture. + Swipez à gauche/droite pour passer à la piste précédente/suivante. Maintenez appuyé pour ouvrir le menu contextuel. Appuyez deux fois pour ajouter aux favoris. + + Suivant + Commencer ! + La musique est partout.\nDésormais, vous n\'avez plus besoin de l\'être. + Arrêtez de chercher votre musique partout sur le web - de services que vous n\'utilisez pas en sources auxquelles vous n\'avez pas accès. Avec juste le titre ou un lien vers une chanson, un album ou une playlist, Tomahawk trouvera la meilleure source disponible et lancera la lecture, tout simplement. + Vous vous sentez social ? + Avec Hatchet, vous avez enfin la possibilité d\'avoir toute votre collection de musique au même endroit. Partagez vos playlists et vos morceaux favoris. Ou synchronisez simplement Tomahawk sur votre téléphone et votre ordinateur. + Connexion + C\'est fait ! + C\'est fait ! + Découvrez notre application et n\'hésitez pas à nous dire ce que vous en pensez ! + + Choisissez la playlist à laquelle ajouter %1$d morceau : + Choisissez la playlist à laquelle ajouter %1$d morceaux : + + Désolé, cette source ne gère pas le déplacement dans les morceaux. + Installation de l\'extension + Vous êtes sur le point d\'installer une extension Tomahawk depuis une source inconnue. C\'est un risque de sécurité. + Souhaitez-vous vraiment installer cette extension ? + Désinstallation de l\'extension + Souhaitez-vous vraiment désinstaller cette extension ? + Choisissez un compte : + Fichier impossible à ouvrir. + Album inconnu + Artiste inconnu + Impossible d\'accéder aux paramètres de votre appareil. Merci d\'autoriser l\'accès à vos notifications dans vos paramètres systême. + Attention : Vous êtes sur le point de télécharger un plugin contenant des sources propriétaires. + Le plugin requis est obsolète ou plus compatible. Souhaitez-vous le mettre à jour ? + Le plugin requis n\'est plus installé. Souhaitez-vous l\'installer maintenant ? + + nouveau + Pause + Lecture + Piste précédente + Piste suivante + Ajouter aux favoris + Retirer des favoris + + En cours de lecture + + Lectures &amp: aléatoire + Chargement de la radio : + Radios suggérées + Mes radios + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 000000000..ad2d034a5 --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,87 @@ + + + Aceptar + Cancelar + Acceder + Saír + Rexistrarse + Rexistrarse & Acceder + Inspeccionar de novo + Almacenamento interno + Pistas + Álbums + Artistas + Usuarios + Maiores éxitos + Mellores álbums + Álbums e EPs + Outros lanzamentos + Detalles da lista + Detalles do álbum + Buscar + Lista de reprodución + Ir ao chou + Deixar de ir ao chou + Ir repetindo + Non ir repetindo + descoñecido + sen pistas + Gardar esta lista + Crear unha lista + + Título + Crear emisora + Buscar artistas/cancións/xéneros + Xéneros + Estilo + Correo/usuario + Usuario + Contrasinal + Confirme o contrasinal + Correo (optativo) + Precísase este campo + Non cadran os contrasinais + Usuario ou contrasinal incorrectos. + O servidor rexeitou a conta. + Non se deu accedido. Asegúrate de estar con conexión.. + Non se permite: a conta este xa se está a usar en algures. + A súa sesión xa rematou. + + Acceder a %1$s + + Saír de %1$s + Cargar a colección + Local + Todo + Elixe os cartafois onde buscar vídeos e son. + Sincronizar os teus datos con %1$s. + Reproducións + Ecualizador + Axustar a saída de son. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-he/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..31f02dbdf --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,101 @@ + + + Oké + Mégsem + Bejelentkezés + Kijelentkezés + Újravizsgálás most + Belső tárhely + Számok + Albumok + Szerzők + Felhasználók + Top Albumok + Album részletek + Keresés + Lejátszási lista + Ismétlés be + Ismétlés ki + ismeretlen + Jelenlegi lista mentése + Lejátszási lista létrehozása + + Cím + Keress előadót/számot/műfajt + Műfajok + E-Mail/Felhasználónév + Felhasználónév + Jelszó + Jelszó megerősítése + E-Mail (választható) + Ezen mező kitöltése kötelező + Jelszó nem egyezik + Hibás felhasználónév vagy jelszó + Akció nem elérhető, a felhasználód már máshol használatban van. + A felhasználód lejárt + + + Helyi válogatás + Helyi + Összes + Dugd be a lejátszáshoz + Fülhallgató csatlakoztatása után autómatikus indítás + Preferált audió minőség + Információ + Alakalmazásverzió + Visszajelzés + Értékeld az alkalmazást + Látogasd meg weboldalunkat + Az e-mail címed + Köszönjük! + Lejátszás + Add a sorhoz + Add a listához + Törlés + + + + Szeret + + Mégsem szeret + Megosztás + Lejátszási lista + Kedvencek + Beállítások + Előadók A-Z + Albumok A-Z + A-Z + Előzmények + Normális + + Legjobb + + + Követés + + + + + + + + + + + Zene + + Hasonló + + Számok + + Tevékenység + + + + Engedélyez + Bezár + + + + + diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 000000000..4c713378c --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,131 @@ + + + OK + Batal + Masuk + Keluar + Daftar + Pindai ulang sekarang + Penyimpanan Internal + Lagu + Album + Artis + Pengguna + Lagu Teratas + Album Teratas + Detail Daftar Putar + Detail Album + Pencarian + Daftar Putar + Pemutaran Acak dinyalakan + Pemutaran Acak dimatikan + Pengulangan dinyalakan + Pengulangan dimatikan + tidak diketahui + tidak ada lagu + Simpan daftar putar saat ini + + E-mail/Nama Pengguna + Nama Pengguna + Kata Kunci + Konfirmasi kata kunci + E-mail (opsional) + Harus diisi + Kata kunci tidak cocok + Nama pengguna atau kata kunci tidak benar + Akun ditolak oleh server + Tidak dapat melakukan autentifikasi. Harap cek koneksi Anda. + Tindakan tidak diperkenankan, akun sedang digunakan di tempat lain. + Akun Anda sudah kedaluarsa. + + + Koleksi Lokal + Lokal + Semua + Pilih direktori yang harus dipindai sebagai media lokal. + Selaraskan data Anda dengan %1$s. + Pemutaran + Scrobble Semua + Scrobble setiap lagu dari setiap applikasi pemutar musik di ponsel dengan %1$s. + Pasang untuk memutar + Mulai memutar lagu sesaat setelah headset terhubung. + Kualitas Suara yang Disukai + Gunakan kualitas audio yang disukai jika tidak terhubung dengan WiFi. + Info + Versi Aplikasi + Umpan balik + Bagikan idemu atau bertanya mengenai bantuan. + Kirim catatan + Tomahawk berhenti beroperasi + Terjadi kesalahan tak terduga yang memaksa aplikasi berhenti. Mohon bantu kami menyelesaikan ini dengan mengirimkan data kesalahan, yang harus Anda lakukan hanyalah klik OK. + Mungkin Anda ingin menambahkan komentar tentang masalah tersebut di bawah: + Terima Kasih ! + Putar + Tambahkan ke Antrian + Tambahkan ke Daftar Putar + Hapus + + Hapus dari Koleksi + + Tambahkan ke Koleksi + + Suka + + Hapus dari daftar suka + Bagi + Umpan + Koleksi + Daftar Putar + Favorit + Setelan + Koleksi Penyimpanan Awan + Baru saja ditambahkan + Artis A-Z + Jumlah putar + Album A-Z + A-Z + Riwayat + Biasa + Bagus + Sangat Bagus + Buka kotak navigasi + Tutup kotak navigasi + + Mengikuti + + Ikuti + + + + + + + + + + + %1$s oleh %2$s + Musik + Biografi + + Mirip + + Lagu + + Aktifitas + + Pengikut + + Mengikuti + + Masuk ke + keluar dari + Ingin menyimpan data seluruh lagu yang Anda putar di Hatcet secara otomatis? + Akan kami lakukan untuk Anda! Mohon aktifkan layanan kami di setelan sistem Anda. + Tutup + Lihat Album + + + + + diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-is/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..7353896e3 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,216 @@ + + + OK + Annulla + Login + Logout + Registrati + Registra & Accedi + Riscansiona ora + Memoria Interna + Tracce + Album + Artisti + Utenti + Top Hit + Top Album + Dettagli Playlist + Dettagli Album + Cerca + Playlist + Riproduzione Casuale attivata + Riproduzione Casuale disattivata + Ripetizione attivata + Ripetizione disattivata + sconosciuto + nessuna traccia + Salva la playlist corrente + Crea una playlist + + Titolo + Email/Username + Nome utente + Password + Conferma la password + Email (opzionale) + Questo campo è richiesto + La password sono diverse + Nome utente o password errati. + Account rifiutato dal server. + Impossibile connettersi. Per favore controlla la connessione. + Azione non permessa, l\'account è in uso altrove. + Il tuo account è scaduto. + + Connesso a %1$s + + Disconnesso da %1$s + Collezione Locale + In Locale + Tutte + Scegli le cartelle dove effettuare la ricerca per i media in locale. + Sincronizza i tuoi dati con %1$s. + Riproduzione + Equalizzatore + Personalizza l\'output audio. + Scrobbla tutto + Scrobbla a %1$s tutte le tracce da tutti i lettori musicali sul telefono. + Inserisci per ascoltare + Inizia a riprodurre musica non appena connetti delle cuffie. + Qualità Audio Preferita + La qualità audio preferita se non connesso a reti WiFi + Informazioni + Versione App + Feedback + Condividi le tue idee e chiedi supporto. + Invia Log + Invia i log per aiutare a risolvere bachi dell\'applicazione. + Descrivere brevemente il problema riscontrato e fornire un indirizzo email per essere contattati in caso di necessità. Grazie! + Il tuo indirizzo email + Problema + Tomahawk si è chiuso inaspettatamente + Si è verificato un errore inatteso che ha forzato la chiusura dell\'applicazione. Per favore aiutaci a correggerlo inviandoci i dettagli, tutto ciò che devi fare è cliccare su OK. + Puoi aggiungere un commento sul problema qui in basso: + Grazie! + Riproduci + Metti in Coda + Aggiungi a Playlist + Elimina + Rimuovi dalla playlist + + Rimuovi da Collezione + + Aggiungi a Collezione + + Ti piace + + Non ti piace più + Condividi + Feed + Collezione + Playlist + Preferiti + Impostazioni + Collezioni in Cloud + Aggiunti di Recente + Artista A-Z + Numero di Riproduzioni + Album A-Z + A-Z + Cronologia + Normale + Ottima + Migliore + Apri Menu Laterale + Chiudi Menu Laterale + + Seguito + + Segui + + Suggerimenti sulle persone da seguire + + %1$s adesso segue + + + A %1$s gli è piaciuta un acanzone + A %1$s gli sono piaciute %2$d canzoni + + + + %1$s ha collezionato una canzone + %1$s ha collezionato %2$d canzoni + + + + %1$s ha collezionato un album + %1$s ha collezionato %2$d album + + + + %1$s ha collezionato un artista + %1$s ha collezionato %2$d artisti + + + %1$s ha commentato su + + + %1$s ha ascoltato assieme a un amico + %1$s ha ascoltato insieme agli amici di %2$d + + + + %1$s ha creato una playlist + %1$s ha creato %2$d playlist + + + %1$s di %2$s + poci secondi fa + + un minuto fa + %1$d minuti fa + + + un\'ora fa + %1$d ore fa + + + un giorno fa + %1$d giorni fa + + %1$s ha ascoltato di recente + I Favoriti di %1$s + La playlist di %1$s + Musica + Biografia + + Simili + + Brani + + + La canzone di %1$d + Le canzoni di %1$d + + Attività + + Seguaci + + Seguiti + + Persone che ti seguono: %1$d, Persone che stai seguendo: %2$d + Connesso a + Disconnesso da + Scarica plugin per + Vuoi che siano automaticamente salvati su Hatchet i dati di tutti i brani che riproduci? + A questo penseremo noi! Per favore abilita il nostro servizio nelle impostazioni di sistema. + Chiudi + Vedi Album + Connettiti + Seleziona i servizi che vuoi utilizzare. Tomahawk funziona meglio se più servizi sono attivati. + Avanzate + Informazioni + Puoi cercare nella canzone tenendo premuto sul bottone Esegui + Un tap per aggiungere la canzone in coda. + Tira su per vedere la playlist corrente. + Tira giu per chiudere l\'area di riproduzione. + Scivola verso sinistra o destra per saltare alla canzone successiva o precedente. Tieni premuto per aprire il menu contestuale. Doppio tap per aggiungere ai favoriti. + + Successiva + Iniziare + La musica è ovunque.\nMa tu non hai bisogno di spostarti. + Non è più necessario cercare la musica nel web - tramite servizi che non usi o tramite fonti a cui non puoi accedere. Dato il nome o il link a una canzone, a un album o una playlist, Tomahawk troverà la miglior fonte disponibile e la suonerà per te. + Vuoi socializzare? + Con Hatchet puoi finalmente avere la tua collezione musicale in un singolo luogo. Condividi le tue playlist e canzoni preferite. Oppure sincronizza semplicemente l\'applicazione con Tomahawk sul tuo pc desktop. + Connettiti + Fatto! + Tutto fatto! + Divertiti ad esplorare l\'applicazione e a dirci cosa ne pensi! + + Scegli una playlist per aggiungere la canzone %1$d: + Scegli una playlist a cui aggiungere %1$d canzoni: + + Spiacente, questo resolver non supporta la ricerca. + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..a9f482927 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,250 @@ + + + OK + キャンセル + ログイン + ログアウト + 登録 + 登録 & ログイン + 今すぐ再スキャン + 内部ストレージ + トラック + アルバム + アーティスト + ユーザー + トップ・ヒット + トップ・アルバム + アルバムとEP + 他のリリース + プレイリスト詳細 + アルバム詳細 + 検索 + プレイリスト + シャッフルがオン + シャッフルがオフ + リピートがオン + リピートがオフ + 不明 + トラックがありません + プレイリストを保存 + プレイリストを作成 + + タイトル + ラジオ局を作成 + アーティスト/曲/ジャンルの検索 + ジャンル + ムード + メール/ユーザー名 + ユーザー名 + パスワード + パスワードを再入力 + メール(省略可能) + 必須フィールドです + パスワードが一致しません + ユーザー名、又はパスワードが間違っています。 + アカウントがサーバーに拒否されました。 + 認証出来ません。接続を確認して下さい。 + この行動が禁止です。アカウントは別の所で使用されています。 + アカウントが切れています。 + + %1$s にログインしました + + %1$s からログアウトしました + ローカルコレクション + ローカル + 全部 + スキャンするローカルメディアのディレクトリーを選択してください。 + %1$sにデータを同期する。 + 再生 + イコライザー + オーディオ出力をカスタマイズする + 全てをScrobbleする + 全ての音楽プレーヤーで再生された全てのトラックを%1$sにScrobbleする。 + 差し込んで再生する + ヘッドセットが繋いだらすぐに音楽を再生する。 + 優先音質 + 無線LANに繋がっていない場合の優先音質 + 情報 + アプリバージョン + フィードバック + ご意見を共有、お問い合わせ等。 + アプリを評価 + Google Play ストアで Tomahawk を評価します。 + 私たちの Web サイトを訪問 + お使いのブラウザーで tomahawk-player.org を開きます。 + ログを送信 + デバッグの目的でログファイルを送信します。 + 発生した問題を簡単に説明してください。必要に応じて私たちがあなたに連絡できるように、メールアドレスを入力してください。ありがとうございます! + あなたのメールアドレス + 問題 + トマホークがクラッシュしました + 予期しないエラーが発生した為、アプリケーションが終了しました。エラーデーターをトマホークチームに送信して、エラーの修正に協力して下さい。OKをクリックするだけで結構です。 + 問題のコメントを以下に入力して下さい + ありがとうございます! + 再生 + キューに追加 + プレイリストに追加 + ラジオ局を作成 + 削除 + プレイリストから削除 + + コレックションから削除 + + コレクションに追加 + + Love + + Loveを取り消す + 共有 + このプレイリストを共有するには Hatchet にログインする必要があります。 + フィード + チャート + コレックション + プレイリスト + ラジオ局 + お気に入り + 設定 + クラウドコレクション + 最近追加した項目 + アーティスト A-Z + 再生回数 + アルバム A-Z + A-Z + 履歴 + 普通 + 良い + 最も良い + ナビゲーションドロワーを開く + ナビゲーションドロワーを閉じる + + フォロー + + フォローする + + フォローするように推薦 + + %1$s がフォローしています + + + %1$s が %2$d のトラックをLoveしました + + + + %1$s が %2$d のトラックを追加しました + + + + %1$s が %2$d のアルバムを追加しました + + + + %1$s が %2$d のアーティストを追加しました + + + %1$s が次の内容にコメントしました + + + %1$s が %2$d の友達と共に聴きました + + + + %1$s が %2$d のプレイリストを作成しました + + + %2$sの%1$s + 数秒前 + + %1$d分前 + + + %1$d時間前 + + + %1$d日前 + + %1$s の最近再生した + %1$s のお気に入り + %1$s のプレイリスト + 私が最近再生した + 私のお気に入り + 私のプレイリスト + 音楽 + 経歴 + + 似た項目 + + + + + %1$d 曲 + + 活動 + + フォロワー + + フォロー + + フォロワー: %1$d, フォロー中: %2$d + 有効 + 無効 + ログインする + ログアウトする + プラグインのダウンロード... + Hatchetで再生したトラックの全てのデータを自動保存しますか? + やらせて下さい。システム設定にサービスをオンにして下さい。 + 閉じる + アルバムを表示 + 接続 + 利用したいサービスを選択してください。 トマホークは、より多くのサービスをアクティブにしたときに快適にご利用できます。 + 手動でインストール + 詳細 + 情報 + 再生ボタンを長押しして、トラックをシークすることができます。 + タップすると、キューにトラックを追加します。 + プルアップすると現在のプレイリストを表示します。 + プルダウンすると再生ドロワーを閉じます。 + 左/右にスワイプすると、次/前のトラックにスキップします。 長押しすると、コンテキストメニューを開きます。ダブルタップすると、お気に入りに追加します。 + + 次へ + 始める + 音楽はどこにでもあります。\n面倒なことをする必要はありません。 + ウェブ上の音楽を追いかけるのはやめましょう - 使わないサービスや、アクセスできないソース。 曲名や、曲、アルバム、プレイリストのリンクを入れると、トマホークはあなたのために最もよいソースを見つけて、再生します。 + 交流しますか? + Hatchet で、最終的に一箇所にあなたの音楽コレクションをまとめることができます。あなたのプレイリストやお気に入りの曲を共有しましょう。それともあなたのデスクトップマシンのトマホークとアプリを同期しますか。 + 接続 + 完了しました! + すべて完了しました! + アプリを楽しんでください。そしてあなたが感じたことを教えてください! + + プレイリストを選択して %1$d のトラックを追加します: + + 申し訳ありません、このリゾルバーはシークをサポートしていません。 + プラグインのインストール + Tomahawk プラグインを不明なソースからインストールしようとしています。これはセキュリティのリスクがあります。 + このプラグインをインストールしてもよろしいですか? + プラグインのアンインストール + このプラグインをアンインストールしてもよろしいですか? + アカウントの選択: + ファイルを開くことができません。 + 不明なアルバム + 不明なアーティスト + お使いのデバイスで設定アプリを見つけることができませんでした。システム設定で Tomahawk に通知へのアクセス権を与えてください。 + 警告: クローズドソースのパーツが含まれたプラグインをダウンロードしようとしています。 + 必要なプラグイン アプリケーションが古く、もはや互換性がありません。今すぐ、それを更新しますか? + 必要なプラグイン アプリケーションがインストールされていません。今すぐインストールしますか? + + 新規 + 一時停止 + 再生 + 前のトラック + 次のトラック + お気に入りに追加 + お気に入りから削除 + + 再生中 + + シャッフル & 再生 + ラジオ局の読み込み中: + 推奨のラジオ局 + マイ ラジオ局 + diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-ka/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-land/constants.xml b/app/src/main/res/values-land/constants.xml new file mode 100644 index 000000000..e9aa3dbdb --- /dev/null +++ b/app/src/main/res/values-land/constants.xml @@ -0,0 +1,9 @@ + + + + true + 3 + 5 + false + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 000000000..c59e7ac24 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,13 @@ + + + + 150dp + 72dp + 104dp + 104dp + 100dp + 32dp + + 64dp + + \ No newline at end of file diff --git a/app/src/main/res/values-large-land/constants.xml b/app/src/main/res/values-large-land/constants.xml new file mode 100644 index 000000000..e3711b2af --- /dev/null +++ b/app/src/main/res/values-large-land/constants.xml @@ -0,0 +1,8 @@ + + + + 5 + 8 + true + + \ No newline at end of file diff --git a/app/src/main/res/values-large-land/dimens.xml b/app/src/main/res/values-large-land/dimens.xml new file mode 100644 index 000000000..fbdba6b6d --- /dev/null +++ b/app/src/main/res/values-large-land/dimens.xml @@ -0,0 +1,11 @@ + + + + 350dp + 112dp + 160dp + 128dp + 200dp + 48dp + + \ No newline at end of file diff --git a/app/src/main/res/values-large/constants.xml b/app/src/main/res/values-large/constants.xml new file mode 100644 index 000000000..57449a256 --- /dev/null +++ b/app/src/main/res/values-large/constants.xml @@ -0,0 +1,8 @@ + + + + 4 + 6 + true + + \ No newline at end of file diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml new file mode 100644 index 000000000..e5e9f8da1 --- /dev/null +++ b/app/src/main/res/values-large/dimens.xml @@ -0,0 +1,9 @@ + + + + 438dp + 128dp + 168dp + 128dp + + \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 000000000..24b68fe12 --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,160 @@ + + + Gerai + Atsisakyti + Prisijungti + Atsijungti + Registruotis + Registruotis ir prisijungti + Peržvelgti iš naujo dabar + Vidinė talpykla + Takeliai + Albumai + Atlikėjai + Naudotojai + Populiariausi takeliai + Populiariausi albumai + Kitos laidos + Išsamiau apie grojaraštį + Išsamiau apie albumą + Paieška + Grojaraštis + Maišymas įjungtas + Maišymas išjungtas + Kartojimas įjungtas + Kartojimas išjungtas + nežinoma + nėra takelio + Įrašyti dabartinį grojaraštį + Sukurti grojaraštį + + Pavadinimas + Žanrai + El. paštas/Naudotojo vardas + Naudotojo vardas + Slaptažodis + Slaptažodžio patvirtinimas + El. paštas (nebūtina) + Šis laukas yra būtinas + Slaptažodžiai nesutampa + Neteisingas naudotojo vardas ar slaptažodis. + Serveris atmetė paskyrą. + Neįmanoma nustatyti tapatybę. Prašome patikrinti savo ryšį. + Veiksmas neleidžiamas, paskyra yra naudojama kitur. + Jūsų paskyra nebegalioja. + + Prisijungta prie %1$s + + Atsijungta nuo %1$s + Vietinė kolekcija + Pasirinkite katalogus, kurie turėtų būti peržvelgiami, ieškant vietinės medijos. + Sinchronizuoti duomenis su %1$s. + Glodintuvas + Informacija + Programos versija + Atsiliepimai + Pasidalinkite savo idėjomis arba paprašykite palaikymo. + Aplankyti mūsų svetainę + Atverti tomahawk-player.org jūsų naršyklėje. + Siųsti žurnalą + Siųsti jūsų žurnalo failą derinimo tikslais. + Prašome trumpai aprašyti problemą, su kuria susiduriate, ir suteikti savo el. pašto adresą, kad, jei būtina, galėtume su jumis susisiekti. Ačiū! + Jūsų el. pašto adresas + Problema + Tomahawk užstrigo + Įvyko netikėta klaida, kuri privertė programą baigti darbą. Prašome mums padėti, išsiunčiant klaidos duomenis, viskas ką jums reikia padaryti, tai nuspausti Gerai. + Žemiau, galite pridėti savo, su problema susijusius, komentarus: + Ačiū ! + Groti + Pridėti į eilę + Pridėti į grojaraštį + Ištrinti + Šalinti iš grojaraščio + + Pašalinti iš kolekcijos + + Pridėti į kolekciją + + + Dalintis + Kanalas + Kolekcija + Grojaraščiai + Nustatymai + Debesijos kolekcijos + Paskiausiai pridėti + Atlikėjai A-Ž + Albumai A-Ž + A-Ž + Istorija + Atverti naršymo stalčių + Užverti naršymo stalčių + + Sekate + + Sekti + + Siūlomi sekti žmonės + + %1$s dabar seka + + + + + + %1$s pakomentavo + + + + %1$s pagal %2$s + prieš keletą sekundžių + Muzika + Biografija + + + Dainos + + + %1$d daina + %1$d dainos + %1$d dainų + + Veikla + + Sekėjai + + Seka + + Įjungti + Išjungti + Prisijungti prie + Atsijungti nuo + Norite automatiškai įrašyti duomenis apie visus takelius, kuriuos grojate per Hatchet? + Užverti + Žiūrėti albumą + Pasirinkite paslaugas, kuriomis norite naudotis. Tomahawk veikia geriau, kuomet yra aktyvuota daugiau paslaugų. + Informacija + + Kitas + Darbo pradžia + Atlikta! + Viskas atlikta! + Nepavyksta atverti failo. + Nežinomas albumas + Nežinomas atlikėjas + + naujas + Pristabdyti + Groti + Ankstesnis takelis + Kitas takelis + Pridėti į mėgstamus + Šalinti iš mėgstamų + + Dabar grojama + + Maišyti ir groti + Įkeliama stotis: + Siūlomos stotys + Mano stotys + diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..c1f74ff92 --- /dev/null +++ b/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,110 @@ + + + OK + Avbryt + Logg på + Logg av + Registrer + Registrer & Logg på + Intern lagring + Låter + Albumer + Artister + Brukere + Andre utgivelser + Søk + Spilleliste + ukjent + ingen låter + Lagre nåværende spilleliste + Opprett spilleliste + + Tittel + E-post/Brukernavn + Brukernavn + Passord + E-post (valgfritt) + Dette feltet er påkrevet + Passordene er ikke like + Brukernavn eller passord er feil. + Konto avvist av tjener. + Kontoen din har utløpt. + + Koblet til %1$s + + Koblet fra %1$s + Lokal samling + Lokal + Spill + Legg til i spilleliste + Slett + Fjern fra spilleliste + + Fjern fra samling + + Legg til i samling + + + Del + Samling + Spillelister + Favoritter + Innstillinger + Skysamlinger + Nylig lagt til + Artist A-Å + Album A-Å + A-Å + + Følger + + + + + + + + + + + + noen få sekunder siden + + ett minutt siden + %1$d minutter siden + + + en time siden + %1$d timer siden + + + en dag siden + %1$d dager siden + + Musikk + Biografi + + Lignende + + Sanger + + + %1$d sang + %1$d sanger + + Aktivitet + + Følgere + + Følger + + Følgere: %1$d, Følger: %2$d + Koble til + Koble fra + Lukk + Vis album + + + + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..8e74db654 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,261 @@ + + + OK + Annuleren + Inloggen + Uitloggen + Registreren + Registreren & Inloggen + Nu opnieuw scannen + Interne opslag + Nummers + Albums + Artiesten + Gebruikers + Topnummers + Topalbums + Albums en EP\'s + Overige uitgaven + Afspeellijstdetails + Albumdetails + Zoeken + Afspeellijst + Willekeurig staat aan + Willekeurig staat uit + Herhalen staat aan + Herhalen staat uit + onbekend + geen nummer + De huidige afspeellijst opslaan + Afspeellijst creëren + + Titel + Station creëren + Zoeken naar artiesten/nummers/genres + Genres + Stemmingen + E-mailadres/gebruikersnaam + Gebruikersnaam + Wachtwoord + Wachtwoordbevestiging + E-mailadres (optioneel) + Dit veld is vereist + Wachtwoorden komen niet overeen + Gebruikersnaam of wachtwoord is incorrect. + Account geweigerd door server. + Kan niet authenticeren. Controleer uw verbinding. + Actie niet toegestaan. Het account wordt elders al gebruikt. + Uw account is verlopen. + + Ingelogd bij %1$s + + Uitgelogd bij %1$s + Lokale verzameling + Lokaal + Alles + Kies de mappen die moeten worden gescand op lokale mediabestanden. + Synchroniseer uw gegevens met %1$s. + Afspelen + Toonregelaar + De audiouitvoer naar wens aanpassen + Van alles muziekgegevens bijhouden + De muziekgegevens bijhouden van elke nummer afgespeeld op elke muziekspeler-app op de telefoon met %1$s. + Plug-in-to-Play + Muziek afspelen zodra een headset is aangesloten. + Gewenste audiokwaliteit. + De gewenste audiokwaliteit indien niet aangesloten op Wi-Fi. + Informatie + App-versie. + Feedback + Ideeen met ons delen or om ondersteuning vragen. + Deze app waarderen + Waardeer Tomahawk in de Google Play Store. + Bezoek onze website + Open tomahawk-player.org in uw webbrowser. + Log versturen + Verstuur uw logbestand voor foutopsporingsdoeleinden + Omschrijf het probleem dat u ervaart en geef een e-mailadres op zodat we contact op kunnen nemen indien nodig. Bedankt! + Uw e-mailadres + Probleem + Tomahawk is vastgelopen. + Er is een onverwachte fout opgetreden waardoor de toepassing niet meer werkt. Help ons dit op te lossen door de foutgegevens naar ons te versturen. Klik daarvoor op OK. + U kunt opmerkingen over het probleem hieronder plaatsen: + Dank u wel! + Afspelen + Aan wachtrij toevoegen + Aan afspeellijst toevoegen + Station creëren + Verwijderen + Verwijderen uit afspeellijst + + Uit verzameling verwijderen + + Aan verzameling toevoegen + + Aan favorieten toevoegen + + Uit favorieten verwijderen + Delen + U dient in te loggen bij Hatchet om deze afspeellijst te kunnen delen. + Feed + Toplijsten + Collectie + Afspeellijsten + Stations + Favorieten + Instellingen + Cloud-verzamelingen + Recent toegevoegd + Artiest A-Z + Aantal keer afgespeeld + Album A-Z + A-Z + Geschiedenis + Normaal + Goed + Beste + Navigatiepaneel openen + Navigatiepaneel sluiten + + Volgt + + Volgen + + Volgsuggesties + + %1$s volgt nu + + + %1$s vond nummer leuk + %1$s vond %2$d nummers leuk + + + + %1$s heeft een nummer verzameld + %1$s heeft %2$d nummers verzameld + + + + %1$s heeft een album verzameld + %1$s heeft %2$d albums verzameld + + + + %1$s heeft een artiest verzameld + %1$s heeft %2$d artiesten verzameld + + + %1$s heeft gereageerd op + + + %1$s luisterde mee met een vriend + %1$s luisterde mee met %2$d vrienden + + + + %1$s creërde een afspeellijst + %1$s creërde %2$d afspeellijsten + + + %1$s van %2$s + een paar seconden geleden + + een minuut geleden + %1$d minuten geleden + + + een uur geleden + %1$d uur geleden + + + een dag geleden + %1$d dagen geleden + + %1$s heeft recent afgespeeld + %1$s\'s favorieten + %1$s\'s afspeellijst + Mijn recentelijk afgespeelde muziek + Mijn favorieten + Mijn afspeellijst + Muziek + Biografie + + Soortgelijk + + Nummers + + + %1$d nummer + %1$d nummers + + Activiteit + + Volgers + + Volgend + + Volgers: %1$d, Volgend: %2$d + Inschakelen + Uitschakelen + Aanmelden bij + Afmelden bij + Plug-in downloaden voor + Wilt u automatisch gegevens opslaan over alle nummer die u afspeelt via Hatchet? + Dat zullen we voor u doen! Graag onze dienst inschakelen in uw systeeminstellingen. + Sluiten + Album weergeven + Verbinden + Selecteer de diensten die u wilt gebruiken. Tomahawk werkt beter wanneer u meer diensten activeert. + Handmatig geïnstalleerd + Geavanceerd + Informatie + U kunt spoelen in een nummer door de afspeelknop ingedrukt te houden. + Raak aan om het nummer toe te voegen aan de wachtrij. + Trek omhoog om de huidige afspeellijst weer te geven. + Trek omlaag om de afspeellijstlade te sluiten. + Veeg naar links/rechts om door te gaan naar het vorige/volgende nummer. Houdt ingedrukt om het contextmenu te openen. 2 keer tikken voegt het nummer toe aan uw favorieten. + + Volgende + Opstartgids + Muziek is overal.\nNu hoeft u dat niet te zijn. + Stop met het zoeken van muziek over heel het internet, bij diensten die u niet gebruikt of niet mag gebruiken. Geef de naam op van of link op naar een nummer, album of afspeellijst en Tomahawk zal de best beschikbare bron voor u vinden en afspelen. + Wilt u sociaal zijn? + Met Hatchet kunt u uw muziekcollectie op één plek al uw muziek vinden. Deel uw afspeellijsten en favoriete nummers. Of synchroniseer de app met Tomahawk op uw computer. + Verbinden + Afgerond! + Alles is afgerond! + Veel plezier met het ontdekken van de app en vergeet niet om uw mening te laten horen aan ons! + + Kies een afspeellijst om %1$d nummer aan toe te voegen: + Kies een afspeellijst om %1$d nummers aan toe te voegen: + + Onze excuses, deze resolver ondersteunt spoelen niet. + Plug-in-installatie + U staat op het punt om een Tomahawk-plug-in te installeren uit een onbekende bron. Dit kan een beveiligingsrisico met zich mee brengen. + Weet u zeker dat u deze plug-in wilt installeren? + Bezig met deïnstalleren van de plug-in + Weet u zeker dat u deze plug-in wilt deïnstalleren? + Kies een account: + Het bestand kan niet worden geopend. + Onbekend album + Onbekende artiest + De Instellingen-app kan niet worden gevonden op uw apparaat. Geef Tomahawk toestemming voor meldingen in uw Systeeminstellingen. + Waarschuwing: u staat op het punt om een plug-in te downloaden die gesloten-bron-onderdelen bevat. + De vereiste plug-in-applicatie is verouderd en niet meer compatibel. Wilt u deze nu bijwerken? + De vereiste plug-in-applicatie is niet meer geïnstalleerd. Wilt u deze nu installeren? + + nieuw + Pauzeren + Afspelen + Vorig nummer + Volgend nummer + Toevoegen aan favorieten + Verwijderen uit favorieten + + Nu aan het afspelen + + Willekeurig & afspelen + Bezig met laden van station: + Aanbevolen stations + Mijn stations + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..8cd7340d4 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,157 @@ + + + OK + Anuluj + Zarejestruj + Przeskanuj ponownie + Pamięć wewnętrzna + Utwory + Albumy + Artysta + Użytkownicy + Najlepsze Hity + Najlepsze Albumy + Inne wydania + Szczegóły Playlisty + Szczegóły Albumu + Wyszukaj + Playlista + Powtarzanie jest włączone + Powtarzanie jest wyłączone + Zapisz obecną playlistę + Utwórz playliste + + Tytuł + Utwórz stację + Gatunki + Nastroje + E-Mail/Nazwa użytkownika + Nazwa użytkownika + Hasło + Potwierdź hasło + E-Mail (opcjonalny) + To pole jest wymagane + Hasła się różnią + Nazwa użytkownika lub hasło jest niepoprawne + Twoje konto wygasło + + Zalogowany do %1$s + + Wylogowany z %1$s + Lokalna kolekcja + Korektor graficzny + Scrobbluj wszystko + Scrobbluj każdy utwór z każdego odtwarzacza na telefonie do %1$s + Zacznij odtwarzać muzykę kiedy słuchawki zostaną podłączone + Preferowana jakość audio + Preferowana jakość audio jeśli telefon nie jest połączony do WiFi + Podziel się pomysłami lub zapytaj support + Oceń aplikację + Oceń Tomahawk w Google Play Store + Odwiedź naszą stronę + Otwórz tomahawk-player.org w przeglądarce + Wyślij dziennik + Proszę krótko opisać problem, który posiadasz oraz podaj email, żeby skontaktować się z Tobą w razie potrzeby. Dziękujemy! + Twój adres email + Dziękuję ! + Dodaj do kolejki + Dodaj do playlisty + Utwórz Stację + Usuń + Usuń z playlisty + + Usuń z kolekcji + + Dodaj do kolekcji + + Polub + + Przestań lubić + Udostępnij + Kolekcje + Playlisty + Stacje + Ulubione + Ustawienia + Kolekcje w chmurze + Ostatnio dodane + A-Z + Historia + Normalna + Dobra + Najlepsza + + Obserwowani + + Obserwuj + + + + + + + + + + + kilka sekund temu + + minutę temu + %1$d minuty temu + %1$d minut temu + %1$d minut temu + + + godzinę temu + %1$d godziny temu + %1$d godzin temu + %1$d godzin temu + + + dzień temu + %1$d dni temu + %1$d dni temu + %1$d dni temu + + Moje Ulubione + Muzyka + Biografia + + + Utwory + + Aktywnośc + + Obserwujący + + Obserwowani + + Włącz + Wyłącz + Zaloguj do + Wyloguj z + Pobierz wtyczkę do + Zrobimy to za Ciebie! Proszę włącz naszą usługę w ustawieniach systemu + Zamknij + Wyświetl album + Połącz z + Stuknij, aby dodać utwór do kolejki + + Gotowe! + Wszystko gotowe! + Na pewno chcesz zainstalować tą wtyczkę? + Usuwanie wtyczki + Wybierz konto: + Nieznany album + Nieznany artysta + + Poprzedni utwór + Następny utwór + Dodaj do ulubionych + Usuń z ulubionych + + Teraz odtwarzane + + Ładowanie stacji: + Moje Stacje + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..7147a5388 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,224 @@ + + + Ok + Cancelar + Entrar + Sair + Registrar + Registrar & Entrar + Escanear novamente + Armazenamento Interno + Faixas + Álbuns + Artistas + Usuários + Top Hits + Top Álbuns + Detalhes da Playlist + Detalhes dos Álbuns + Pesquisar + Playlist + Aleatório ativado + Aleatório desativado + Repetição ativada + Repetição desativada + desconhecido + sem faixa + Salvar a playlist atual + Criar playlist + + Título + Email/Nome de usuário + Nome de usuário + Senha + Confirmação da senha + Email (opcional) + Este campo é obrigatório + As senhas não coincidem + Nome de usuário ou senha incorreto + Conta rejeitada pelo servidor + Não foi possível autenticar. Por favor, verifique a sua conexão. + Ação não permitida, a conta está ativa em outro lugar. + A sua conta expirou. + + Conectado em %1$s + + Desconectado de %1$s + Coleção Local + Local + Todos + Escolha os diretórios que deverão ser examinados para mídia local. + Sincronizar seus dados com %1$s. + Reprodução + Equalizador + Personalizar a saída de áudio. + Fazer Scrobble de Tudo + Fazer Scrobble de todas as faixas de todos os players de música no celular para %1$s + Conectar para Tocar + Iniciar música assim que o fone de ouvido é conectado. + Qualidade Preferencial do Áudio + A qualidade preferencial do áudio se não estiver conectado ao WiFi. + Informação + Versão do Aplicativo + Comentário + Compartilhe suas ideias ou pergunte para o suporte + Enviar Registro + Envie seu arquivo de registros para fins de análise de erros. + Por favor descreva brevemente o problema que você está tendo juntamente com seu email para que possamos te contatar caso necessário. Obrigado! + Seu endereço de email + Problema + O Tomahawk travou + Um erro inesperado ocorreu, forçando a interrupção da aplicação. Por favor, nos ajude a arrumar isso enviando os dados do erro. Tudo que você precisa fazer é clicar em OK. + Você pode adicionar comentários sobre seu problema abaixo: + Obrigado! + Reproduzir + Adicionar à Fila + Adicionar a Playlist + Deletar + Remover da Playlist + + Remover da Coleção + + Adicionar a Coleção + + Curtir + + Descurtir + Compartilhar + Feed + Coleção + Playlists + Favoritos + Configurações + Coleções na Nuvem + Recentemente Adicionado + Artista A-Z + Total de Reproduções + Álbum A-Z + A-Z + Histórico + Normal + Bom + Ótimo + Abrir Gaveta de Navegação + Fechar Gaveta de Navegação + + Seguindo + + Seguir + + Pessoas sugeridas para seguir + + %1$s agora segue + + + %1$s curtiu uma faixa + %1$s curtiu %2$d faixas + + + + %1$s coletou uma faixa + %1$s coletou %2$d faixas + + + + %1$s coletou um álbum + %1$s coletou %2$d álbuns + + + + %1$s coletou um artista + %1$s coletou %2$d artistas + + + %1$s comentou em + + + %1$s ouviu junto com um amigo + %1$s ouviu junto com %2$d amigos + + + + %1$s criou uma playlist + %1$s criou %2$d playlists + + + %1$s por %2$s + há alguns segundos + + um minuto atrás + %1$d minutos atrás + + + uma hora atrás + %1$d horas atrás + + + um dia atrás + %1$d dias atrás + + Tocado recentemente por %1$s + Favoritos de %1$s + Playlists de %1$s + Música + Biografia + + Similar + + Músicas + + + %1$d Música + %1$d Músicas + + Atividade + + Seguidores + + Seguindo + + Seguidores: %1$d, Seguindo: %2$d + Entrar em + Sair de + Baixar Plugin para + Você quer salvar automaticamente dados sobre todas as faixas que você toca no Hatchet? + Faremos isso para você! Por favor, habilite nosso serviço nos ajustes do seu sistema. + Fechar + Visualizar Álbum + Conectar + Selecione os serviços que você quer usar. O Tomahawk funciona melhor com mais seviços ativados. + Instalado manualmente + Avançado + Informação + Você pode avançar/retroceder na faixa tocando e segurando o botão Play. + Toque para adicionar a faixa à fila. + Empurre para cima para ver a playlist atual + Empurre para baixo para fechar a tela de reprodução + Arraste para a esquerda/direita para pular para a próxima faixa ou a anterior. Toque e segure para abrir o menu de contexto. Toque duas vezes para adicionar aos seus favoritos. + + Próximo + Começando + A música está por todos os lados.\nAgora você não precisa estar. + Pare de ficar caçando músicas pela internet - de serviçoes que você não usa, ou fontes as quais você não tem acesso. Dê o nome ou link de uma música, álbum ou playlist, e o Tomahawk irá encontrar a melhor fonte disponível para você e toca-la. + Quer socializar? + Com o Hatchet você pode finalmente ter sua coleção favorita de músicas em um único lugar. Compartilhe suas playlists e faixas favorites. Ou simplesmente sincronize o Aplicativo com o Tomahawk em seu computador. + Conectar + Pronto! + Tudo pronto! + Divirta-se explorando o aplicativo e nos conte o que você acha! + + Escolha uma playlist para adicionar %1$d: + Escolha uma playlist para adicionar %1$d faixas: + + Desculpe, esta fonte não permite avançar/retroceder pela música. + Instalação de plugin + Você está prestes a instalar um plugin do Tomahawk proveniente de fonte desconhecida. Isto pode ser um risco de segurança. + Você deseja realmente instalar este plugin? + Desinstalando Plugin + Você deseja realmente desinstalar este plugin? + Escolher uma conta: + Não foi possível abrir o arquivo. + + + + diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000..24b056113 --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,261 @@ + + + OK + Cancelar + Iniciar sessão + Fechar sessão + Registar + Registar e iniciar sessão + Reanalisar agora + Armazenamento interno + Faixas + Álbuns + Artistas + Usuários + Melhores faixas + Melhores álbuns + Álbuns e EP + Outros lançamentos + Detalhes da lista de reprodução + Detalhes do álbum + Busca + Lista de reprodução + Modo aleatório ativo + Modo aleatório inativo + Repetição ativa + Repetição inativa + desconhecido + nenhuma faixa + Guardar lista de reprodução + Criar lista de reprodução + + Título + Criar estação + Procurar artistas/faixas/géneros + Géneros + Ambientes + E-Mail/Usuário + Nome de usuário + Palavra-passe + Confirmação de palavra-passe + E-Mail (opcional) + Este campo é obrigatório + As palavras-passe não são iguais + Nome de utilizador ou palavra-passe inválida + Conta recusada pelo servidor + Autenticação não efetuada. Verifique a sua ligação. + Ação não permitida. A conta está a ser utilizada noutro local. + A sua conta expirou. + + Sessão iniciada em %1$s + + Sessão terminada em %1$s + Coleção local + Local + Tudo + Escolha os diretórios locais que devem ser analisados + Sincronizar dados com %1$s + Reprodução + Equalizador + Personalizar o sistema de som + Enviar tudo + Enviar todas as faixas de todas as aplicações de músicas para %1$s + Iniciar ao introduzir + Inicia a reprodução assim que colocar os auriculares + Qualidade de áudio + Qualidade áudio se não estiver ligado a WiFi + Informações + Versão + Comentários + Partilhe as suas ideias ou solicite ajuda + Avaliar a Aplicação + Avaliar o Tomahawk na Google Play Store. + Visite o nosso Website + Abrir tomahawk-player.org no seu navegador. + Enviar registos + Enviar ficheiro de registo para depuração + Descreva o erro que está a ocorrer e disponibilize um e-mail para que o possamos contactar, se necessário. Obrigado! + O seu endereço de e-mail + Erro + O Tomahawk terminou + Ocorreu um erro inesperado que obrigou a aplicação a encerrar. Ajude-nos a corrigir este problema, enviando-nos os dados do erro. Tudo o que precisa fazer é clicar em OK. + Pode adicionar alguns comentários em baixo: + Obrigado! + Reproduzir + Adicionar à fila + Adicionar à lista de reprodução + Criar Estação + Eliminar + Remover da lista de reprodução + + Remover da coleção + + Adicionar à coleção + + Gosto + + Não gosto + Partilhar + Precisa de iniciar sessão no Hatchet para poder partilhar esta lista de reprodução. + Fonte + Listas + Coleção + Listas de reprodução + Estações + Favoritas + Definições + Coleções na nuvem + Adições recentes + Artista A-Z + N.º de reproduções + Álbum A-Z + A-Z + Histórico + Normal + Boa + Melhor + Abrir menu de navegação + Fechar menu de navegação + + A seguir + + Seguir + + Sugestão de pessoas a seguir + + %1$s está a seguir + + + %1$s gostou de uma faixa + %1$s gostou de %2$d faixas + + + + %1$s obteve uma faixa + %1$s obteve %2$d faixas + + + + %1$s obteve um álbum + %1$s obteve %2$d álbums + + + + %1$s obteve um artista + %1$s obteve %2$d artistas + + + %1$s comentou em + + + %1$s ouviu com 1 amigo + %1$s ouviu com %2$d amigos + + + + %1$s criou uma lista de reprodução + %1$s criou %2$d listas de reprodução + + + %1$s de %2$s + há alguns segundos + + há um minuto + há %1$d minutos + + + há uma hora + há %1$d horas + + + há um dia + há %1$d dias + + %1$s reproduziu recentemente + Favoritas de %1$s + Lista de reprodução de %1$s + As minhas reproduções recentes + As minhas Favoritas + A minha Lista de reprodução + Música + Biografia + + Semelhantes + + Faixas + + + %1$d faixa + %1$d faixas + + Atividade + + Seguidores + + Seguindo + + Seguidores: %1$d, seguindo: %2$d + Ativar + Desativar + Iniciar sessão em + Fechar sessão em + Transferir suplemento para + Gostaria de guardar os dados sobre as faixas reproduzidas no Hatchet? + Podemos fazer isso por si! Ative o serviço nas definições do sistema. + Fechar + Ver álbum + Serviços + Selecione os serviços que quer utilizar. Quantos mais serviços, melhor o Tomahawk se torna. + Instalados manualmente + Avançado + Informações + Pode percorrer uma faixa através de um toque longo no botão Reproduzir + Toque para adicionar a faixa à fila + Deslize acima para ver a lista de reprodução + Deslize abaixo para fechar o menu de navegação + Deslize para a esquerda/direita para recuar/avançar uma faixa. Clique longo para abrir o menu de contexto. Duplo clique para adicionar aos favoritos. + + Seguinte + Iniciação + A música está em todo o lado.\nMas você não tem de estar. + Pare de procurar a música na Internet, tanto em serviços que não utiliza como em fontes a que não tem acesso. Indique o nome ou ligação de uma música e o Tomahawk irá encontrar a melhor fonte para a reproduzir. + Gostaria de socializar? + Através do Hatchet, todas as suas músicas podem estar num único sítio. Partilhe as suas listas de reprodução e faixas favoritas. Ou sincronize a aplicação com o Tomahawk no seu computador. + Ligação + Terminado! + Tudo terminado! + Divirta-se a utilizar a aplicação e diga-nos o que dela acha! + + Escolha a lista de reprodução para adicionar %1$d faixa: + Escolha a lista de reprodução para adicionar %1$d faixas: + + Desculpe mas este \"resolver\" não permite procura. + Instalação de plugin + Está prestes a instalar um plugin do Tomahawk a partir de uma fonte desconhecida. Isto pode representar um risco de segurança. + Deseja mesmo instalar este Plugin? + A desinstalar o plugin + Deseja mesmo desinstalar este Plugin? + Escolha uma conta: + Não é possível abrir o ficheiro. + Álbum desconhecido + Artista desconhecido + Não foi possível encontrar a Aplicação de Definições do seu dispositivo. Por favor dê ao Tomahawk o acesso às suas notificações nas suas Definições do Sistema. + Aviso: Está prestes a transferir um plugin que contém partes de código fechado. + A aplicação plugin necessária está desatualizada e já não é compatível. Deseja atualizá-la agora? + A aplicação plugin necessária já não está instalada. Deseja instalá-la agora? + + nova + Pausa + Reprodução + Faixa Anterior + Faixa Seguinte + Adicionar às Favoritas + Remover das Favoritas + + A Reproduzir + + Misturar & Reproduzir + A carregar a estação: + Estações Sugeridas + As minhas Estações + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 000000000..9ddf22167 --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,73 @@ + + + OK + Renunță + Piese + Albume + Artiști + Utilizatori + Cântece de top + Albume de top + Detalii listă + Detalii album + Caută + Listă de redare + necunoscut + nicio piesă + Salvează lista curentă + + Email/Utilizator + Utilizator + Parolă + Confirmare parolă + Email (opțional) + Acest câmp este obligatoriu + Parolele nu se potrivesc + Utilizator sau parolă incorecte. + Contul a fost respins de server. + + + Colecție locală + Local + Tot + + + + + Colecție + Liste de redare + Favorite + Configurări + Colecții în cloud + Adăugate recent + Artist A-Z + Numărul de redări + Album A-Z + A-Z + Istoric + Normal + Bun + Cel mai bun + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..86c8933bb --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,263 @@ + + + ОК + Отмена + Вход + Выход + Регистрация + Зарегистрироваться & Войти + Пересканировать сейчас + Внутренняя память + Песни + Альбомы + Исполнители + Пользователи + Top Хитов + Top Албомы + Альбомы и EP + Другие версии + Информация о Плейлисте + Информация о Альбоме + Поиск + Плейлист + Перемешивание включено + Перемешивание выключено + Повтор включен + Повтор выключен + Неизвестно + Нет песни + Сохранить в текущий плейлист + Создать плейлист + + Название + Создать станцию + Поиск артистов/песен/жанров + Жанры + Настроение + e-mail/имя пользователя + Имя пользователя + Пароль + Пароль повторно + E-mail (необязательно) + Это поле обязательно + Пароли не совпадают + Имя пользователя или пароль неверны. + Аккаунт не принят сервером. + Невозможность аутентификации. Пожалуйста, проверьте ваше соединение. + Действие не разрешено, аккаунт уже используется. + Ваш аккаунт истёк. + + Вошли в %1$s + + Вышли из %1$s + Локальная коллекция + Локальная + Все + Выберет папку где находиться ваша коллекция. + Синхронизируйте ваши данные с %1$s. + Воспроизведение + Эквалайзер + Настроить аудио выход + Скробблинг всего + Скробблинг каждого трека из каждого плеера в телефоне на %1$s. + Включи и играй + Воспроизводить музыку при подключениии наушников. + Предпочтительное качество звука + Предпочтительное качество звука при отсутствии подключения по WiFi. + Информация + Версия приложения + Обратная связь + Поделитесь идеями или спросите о поддержке + Оцените приложение + Оцените Tomahawk в Google Play Маркете. + Посетите наш сайт + Открыть tomahawk-player.org в вашем браузере. + Отправить логи + Отправить ваши логи для отладки + Пожалуйста, кратко опишите проблему, которая у вас возникла и укажите ваш e-mail, чтобы мы могли связаться с вами. Спасибо! + Ваш email + Ошибка + Tomahawk прекратил работу + Неожиданная ошибка прервала работу программы. Пожалуйста, помогите нам исправить это, отослав нам данные об ошибке. Всё, что от вас необходимо - это нажать ОК. + Вы можете добавить ниже ваши комментарии о проблеме: + Спасибо! + Воспроизведение + Добавить в очередь + Добавить в плейлист + Удалить + Удалить из Плейлиста + + Удалить из коллекции + + Добавить в коллекцию + + Любимая + + Не любимая + Поделиться + Вы должны войти в Hatchet, чтобы поделиться этим списком воспроизведения. + Фид + Чарты + Коллекция + Плейлисты + Станции + Избранное + Настройки + Облачная коллекция + Последняя добавленая + Исполнитель от А до Я + Количество воспроизведений + Альбом от А до Я + От А до Я + История + Нормально + Хорошо + Отлично + Открыть панель навигации + Закрыть панель навигации + + Читаемые + + Подписаться + + Рекомендованные люди + + %1$s подписался + + + %1$s оценил песеню + %1$s оценил %2$d песен + %1$s оценил %2$d песен + %1$s оценил %2$d песен + + + + %1$s добавил песеню + %1$s добавил %2$d песен + %1$s добавил %2$d песен + %1$s добавил %2$d песен + + + + %1$s добавил альбом + %1$s добавил %2$d альбомов + %1$s добавил %2$d альбомов + %1$s добавил %2$d альбомов + + + + %1$s добавил артиста + %1$s добавил %2$d артистов + %1$s добавил %2$d артистов + %1$s добавил %2$d артистов + + + %1$s прокомментировал + + + %1$s послушал вместе с другом + %1$s послушал вместе с %2$d друзьями + %1$s послушал вместе с %2$d друзьями + %1$s послушал вместе с %2$d друзьями + + + + %1$s создал пейлист + %1$s создал %2$d пейлистов + %1$s создал %2$d пейлистов + %1$s создал %2$d пейлистов + + + %1$s от %2$s + пару секунд назад + + минуту назад + %1$d минут назад + %1$d минут назад + %1$d минут назад + + + час назад + %1$d часов назад + %1$d часов назад + %1$d часов назад + + %1$s недавно слушал + %1$s избранное + %1$s плейлист + Мои недавно прослушанные + Моё избранное + Мой Плейлист + Музыка + Биография + + Похожие + + Песни + + + %1$d песню + %1$d песен + %1$d песен + %1$d песен + + Действие + + Читающие + + Читаемые + + Подписчики: %1$d, Подписки: %2$d + Войти в + Выйти из + Скачать плагин для + Хотите автоматически скробблировать играемые треки в Hatchet? + Мы сделаем это для вас! Пожалуйста, включите наш сервис в настройках вашей системы. + Закрыть + Посмотреть Альбом + Соединиться + Выберите сервисы, которые хотели бы добавить. Tomahawk работает тем лучше, чем больше сервис активировано. + Установлено обычным образом + Расширенные + Информация + Вы можете перематывать песню долгим нажатием на кнопку Воспроизведения. + Нажмите, чтобы добавить песню в очередь. + Потяните вверх, чтобы увидеть текущий плейлист. + Потяните вниз, чтобы закрыть окно воспроизведения. + Смахните налево или направо, чтобы включить следующую или предыдущую песню. Долгое нажатие, чтобы открыть меню. Двойное нажатие, чтобы добавить в избранное. + + Следующая + Начиная работу + Музыка повсюду.\nИ теперь Вам не придётся за ней повсюду гоняться. + Хватит выискивать музыку по всему интернету - сервисам, которые Вы не используете или к которым не имеете доступа. Лишь по имени или ссылке на песню, альбому или плейлисту, Tomahawk найдёт песню в лучшем качестве и включит её. + Хотите войти через соц. сети? + С Hatchet Вы, наконец, можете иметь свою музыкальную коллекцию собранную в одном месте. Делитесь списками и любимыми песнями. Или просто синхронизируйте Приложение с Tomahawk на вашем компьютере. + Подключиться + Выполнена + Все задачи выполнены + Весёлого Вам использования нашего приложения и не забудьте сказать, что Вы о нём думаете! + + Выберите плейлист, в который надо добавить %1$d песню: + Выберите плейлист, в который надо добавить %1$d песни: + Выберите плейлист, в который надо добавить %1$d песен: + Выберите плейлист, в который надо добавить %1$d песен: + + Извините, это дополнение не поддерживает поиск. + Установка плагинов + Вы хотите добавить в Tomahawk плагин из неизвестного источника. Это небезопасно. + Вы действительно хотите установить этот Плагин? + Удалить плагин + Вы действительно хотите удалить этот Плагин? + Выберите аккаунт: + Невозможно открыть файл. + Неизвестный альбом + Неизвестный исполнитель + Не найдено Приложение Настроек на вашем девайсе. Пожалуйста, предоставьте Tomahawk доступ к уведомлениям в Системных Настройках. + Внимание: Вы загрузите плагин с частью закрытого кода. + Требуемые плагин устарел и больше не совместим. Вы хотите обновить его сейчас? + Требуемый плагин не установлен. Вы хотите установить его сейчас? + + новое + + + diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-sq/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..9d867b248 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,242 @@ + + + OK + Avbryt + Logga in + Logga ut + Registrera + Registrera & Logga in + Omskanna nu + Intern lagring + Spår + Album + Artister + Användare + Top Hits + Top Album + Album och EPs + Andra släpp + Information om spellistan + Information om albumet + Sök + Spellista + Slumpvis är på + Slumpvis är av + Upprepning är på + Upprepning är av + Okänd + Ingen låt + Spara aktiv spellista + Skapa spellista + + Titel + Skapa station + Sök på artister/låtar/genre + Genre + Humör + E-post/Användarnamn + Användarnamn + Lösenord + Bekräfta lösenord + E-post (frivillig) + Detta fält måste fyllas i + Lösenorden överensstämmer inte + Användarnamn eller lösenord stämmer inte + Ditt kontot godkändes inte av servern. + Kunde inte logga in. Se om du är uppkopplad. + Kommandot är inte tillåtet, ditt konto används på en annan enhet/plats. + Ditt konto har gått ut. + + Loggade in i %1$s + + Loggade ut ur %1$s + Enhetens samling + Lokalt + Alla + Välj + Synkroniserar med %1$s. + Uppspelning + Equalizer + Scrobbla allt + Scrobbla alla låtar från alla musikspelare på telefonen till %1$s. + Koppla in och spela + Spela musiken när headset kopplas in. + Önskad ljudkvalitet + Önskad ljudkvalitet när inte uppkopplad på WIFI. + Info + App-Version + Feedback + Dela dina idéer eller fråga efter hjälp. + Betygsätt appen + Betygsätt Tomahawk på Google Play Butik + Besök vår Hemsida + Öppna tomahawk-player.org I din webbläsare + Skicka logg + Din e-postadress + Problem + Tomahawk har kraschat + Ett oväntat fel har inträffat och tvingat programmet att avslutas. Hjälp oss fixa det genom att skicka datan om felet, allt du behöver göra är att trycka OK. + Skriv din kommentar om felet (frivilligt): + Tack! + Spela + Lägg till i kön + Lägg till i spellistan + Ta bort + Ta bort från spellista + + Ta bort från samling + + Lägg till i samling + + Älska + + Ogilla + Dela + Du behöver logga in på Hatchet för att kunna dela denna spellista. + Flöde + Topplistor + Samling + Spellista + Stationer + Favoriter + Inställningar + Molmsamlingar + Nyligen tillagda + Artister A-Z + Antal spelningar + Album A-Z + A-Z + Historik + Normal + Bra + Bäst + Öppna navigering + Stäng navigering + + Följer + + Följ + + Förslag på personer att följa + + %1$s följer nu + + + %1$s älskade ett spår + %1$s älskade %2$d spår + + + + %1$s samlade ett spår + %1$s samlade %2$d spår + + + + %1$s samlade ett album + %1$s samlade %2$d album + + + + %1$s samlade en artist + %1$s samlade %2$d artister + + + %1$s kommenterade på + + + %1$s lyssnade tillsammans med en vän + %1$s lyssnade tillsammans med %2$d vänner + + + + %1$s skapade en spellista + %1$s skapade %2$d spellistor + + + %1$s av %2$s + några sekunder sedan + + en minut sedan + %1$d minuter sedan + + + en timme sedan + %1$d timmar sedan + + + en dag sedan + %1$d dagar sedan + + %1$s\'s senaste spelat + %1$ss favoriter + %1$ss spellistor + Mina senaste spelningar + Mina favoriter + Min spellista + Musik + Biografi + + Liknande + + Låtar + + + %1$d låt + %1$d låtar + + Aktivitet + + Följare + + Följer + + Följare: %1$d, Följer: %2$d + Logga in på + Logga ut från + Ladda ner plugin för + Vill du automatisk spara all data om låtarna du spelar till Hatchet? + Vi fixar det åt dig! Aktivera vår tjänst i systeminställningarna. + Stäng + Se album + Anslut + Manuellt installerad + Avancerat + Information + Tryck för att lägga till spåret till kön. + Dra upp för att se den nuvarande spellistan. + Dra ner för att stänga spelaren. + + Nästa + Kom igång + Musik finns överallt.\nNu behöver inte du vara det. + Vill du socialisera? + Anslut + Färdig! + Allt klart! + Ha kul utforska appen och säg gärna vad du tycker! + + Välj en spellista att lägga till %1$d spår till: + Välj en spellista att lägga till %1$d spår till: + + Plugin-installation + Du håller på att installera en Tomahawk plugin från en okänd källa. Detta kan vara en säkerhetsrisk. + Är du säker på att du vill installera detta plugin? + Avinstallerar plugin + Är du säker på att du vill avinstallera detta plugin? + Välj ett konto: + Kan inte öppna fil. + Okänt album + Okänd artist + + ny + Paus + Spela + Förra låten + Nästa låt + Lägg till i Favoriter + Ta bort från Favoriter + + Spelas nu + + Laddar station: + diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 000000000..14f8d9334 --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,120 @@ + + + ตกลง + ยกเลิก + เข้าสู่ระบบ + ออกจากระบบ + ลงทะเบียน + แทร็ค + อัลบั้ม + ศิลปิน + ผู้ใช้ + ฮิตสุด + อัลบั้มยอดนิยม + รายละเอียดบัญชีการเล่น + รายละเอียดอัลบั้ม + ค้นหา + บัญชีการเล่น + การเล่นสุ่ม เปิด + การเล่นสุ่ม ปิด + การเล่นซ้ำ เปิด + การเล่นซ้ำ ปิด + ไม่ทราบ + ไม่มีแทร็ค + บันทึกบัญชีการเล่นปัจจุบัน + + อีเมล/ชื่อผู้ใช้ + ชื่อผู้ใช้ + รหัสผ่าน + ยืนยันรหัสผ่าน + อีเมล (ไม่จำเป็น) + ต้องการค่าช่องนี้ + รหัสผ่านไม่ตรงกัน + ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง + บัญชีถูกปฏิเสธโดยผู้ให้บริการ + ไม่สามารถรับรองความถูกต้องได้ โปรดตรวจสอบการเชื่ีอมต่อของคุณ + ไม่อนุญาตการกระทำ บัญชีกำลังถูกใช้อยู่ที่อื่น + บัญชีของคุณหมดอายุแล้ว + + + คอลเลกชั่นท้องถิ่น + ซิงค์ข้อมูลของคุณกับ %1$s + การเล่น + สคร็อบเบิ้ลทุกสิ่ง + สคร็อบเบิ้ลทุกแทร็คจากทุกแอพพ์เครื่องเล่นดนตรีบนโทรศัพท์สู่ %1$s + เสียบเข้าเพื่อเล่น + เริ่มเล่นดนตรีทันทีที่ชุดหูฟังถูกเชื่อมต่อ + คุณภาพเสียงที่ชอบ + คุณภาพเสียงที่ชอบถ้าไม่ได้เชื่อมต่อ WiFi + ข้อมูล + เวอร์ชันแอพพ์ + การตอบรับ + แบ่งปันความคิดเห็นหรือขอความช่วยเหลือ + Tomahawk เกิดการบกพร่อง + เกิดความผิดพลาดที่ไม่พึงปรารถนาขึ้นทำให้แอพพ์หยุดการทำงาน โปรดช่วยเราแก้ไขปัญหานี้โดยส่งข้อมูลความผิดพลาดให้เรา เพียงแค่คุณคลิก ตกลง + คุณอาจเพิ่มความคิดเห็นเกี่ยวกับปัญหาที่ด้านล่าง: + ขอบคุณ! + เล่น + เพิ่มสู่คิว + เพิ่มสู่บัญชีการเล่น + ลบ + + ลบจากการสะสม + + เพิ่มสู่การสะสม + + ชอบ + + เลิกชอบ + แชร์ + ฟีด + การสะสม + บัญชีการเล่น + รายการโปรด + การตั้งค่า + ถูกเพิ่มเมื่อเร็วๆนี้ + จำนวนที่เล่น + ประวัติ + ปกติ + ดี + ดีที่สุด + เปิด Navigation Drawer + ปิด Navigation Drawer + + กำลังติดตาม + + + + + + + + + + + + %1$s โดย %2$s + ดนตรี + ชีวประวัติ + + คล้าย + + เพลง + + กิจกรรม + + ผู้ติดตาม + + กำลังติดตาม + + เข้าสู่ระบบ + ออกจากระบบ + ต้องการบันทึกข้อมูลอัตโนมัติเกี่ยวกับแทร็คที่คุณเล่นทั้งหมดบน Hatchet หรือไม่? + เราจะทำสิ่งนั้นให้คุณ! โปรดเลือกใช้งานบริการของเราในการตั้งค่าระบบ + ปิด + ดูอัลบั้ม + + + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..c44cb37e6 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,135 @@ + + + Tamam + İptal + Giriş + Çıkış + Kayıt + Yeniden Tara + Dahili Depolama + Parçalar + Albümler + Sanatçılar + Kullanıcılar + En İyi Hitler + En iyi Albümler + Çalma Listesi Detayları + Albüm Detayları + Ara + Çalma Listesi + Karışık çalma açık + Karışık çalma kapalı + Tekrarlama açık + Tekrarlama kapalı + bilinmeyen + parça yok + Mevcut çalma listesini kaydet + + E-Posta/Kullanıcı Adı + Kullanıcı Adı + Parola + Parola doğrulama + E-Posta (isteğe bağlı) + Bu alan gereklidir + Parolalar eşleşmiyor + Kullanıcı adı veya parola yanlış. + Hesap sunucu tarafından reddedildi. + Kimlik doğrulanamadı. Lütfen bağlantınızı kontrol edin. + Eylem gerçekleştirilemedi, hesap başka bir yerde kullanılıyor. + Hesabınızın süresi doldu. + + + Yerel Koleksiyon + Yerel + Tümü + Yerel medya için taranması gereken dizinleri seçin. + Verilerinizi %1$s ile senkronize edin. + Oynatma + Her şeyi Skropla + Telefondaki tüm müzik çalar uygulamalarındaki tüm parçaları %1$s \'e skropla. + Çalmak için Takın + Kulaklık bağlanır bağlanmaz müzik çalmaya başlar. + Tercih Edilen Ses Kalitesi + WiFi\'ye bağlı olunmadığında tercih edilecek ses kalitesi. + Bilgi + Uygulama Sürümü + Geribildirim + Fikirlerinizi paylaşın veya destek isteyin. + Kayıtları Gönder + Tomahawk çöktü + Beklenmedik bir hata uygulamanın kapanmasına neden oldu. Lütfen hata verilerini bize göndererek bunu çözmemize yardımcı olun, tek yapmanız gereken Tamam\'a basmak. + Aşağıya problemle ilgili yorumlarınızı ekleyebilirsiniz: + Teşekkürler ! + Oynat + Sıraya Ekle + Çalma Listesine Ekle + Sil + + Koleksiyondan Sil + + Koleksiyona Ekle + + Beğen + + Beğenme + Paylaş + Besle + Koleksiyon + Çalma Listeleri + Favoriler + Ayarlar + Bulut Koleksiyonları + Son Eklenenler + Sanatçı A-Z + Çalma Sayısı + Albüm A-Z + A-Z + Geçmiş + Normal + İyi + En iyi + Navigasyon Çekmecesini Aç + Navigasyon Çekmecesini Kapat + + Takip ediliyor + + Tekip Et + + + + + + + + + + + %2$s den %1$s + Müzik + Biyografi + + Benzer + + Şarkılar + + Etkinlikler + + Takipçiler + + Takip ediliyor + + Oturum aç + Oturumu kapat + Hatchet\'te çaldığın tüm parça bilgilerinin otomatik olarak kaydedilmesini ister misiniz? + Bunu sizin için yapacağız! Lütfen sistem ayarlarınızdan bizim servisimizi etkinleştirin. + Kapat + Albümü Görüntüle + Bağlan + Kullanmak istediğin servisi seç. Daha fazla hizmet etkinleştirildiğinde Tomahawk daha iyi çalışır. + Gelişmiş + Bilgi + + + + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..7a9f9e28c --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,251 @@ + + + Гаразд + Скасувати + Вхід + Вихід + Реєстрація + Зареєструватися & Увійти + Пересканувати знову + Внутрішня пам’ять + Композиції + Альбоми + Виконавці + Користувачі + Кращі Вершні + Кращі Альбоми + Альбоми і EP + Иньші версії + Подробиці про Грайлист + Подробиці про Альбом + Пошук + Грайлист + Довільний порядок увімкнено + Довільний порядок вимкнено + Повторення увімкнено + Повторення вимкнено + Невідомо + Немає пісні + Зберегти поточний грайлист + Створити грайлист + + Назва + Створити станцію + Пошук виконавців/пісень/жанрів + Жанри + Настрій + Пошта/Ім’я користувача + Ім’я користувача + Пароль + Схвалити пароль + E-Mail (необов’язково) + Це поле є обов’язковим + Паролі не збігаються + Ім’я користувача або пароль хибні + Обліковку відхилено сервером. + Неможливо автентифікуватися. Будь ласка, перевірте Ваше з’єднання. + Дія не дозволена, обліковий запис вже використовується. + Термін дії Вашої обліковки минув. + + Увійшли в %1$s + + Вишли із %1$s + Локальна колекція + Локальна + Усі + Виберіть теку де знаходиться Ваша колекція. + Синхронізуйте Ваші дані з %1$s. + Відтворення + Еквалайзер + Налаштувати аудіо вихід + Скробблінг Усього + Скробблінг кожної доріжки з кожного програвача у телефоні на %1$s. + Увімкни і Відтворюй + Починати відтворювати музику як тільки під’єднається гарнітура. + Бажана Якість Звуку + Бажана якість звуку якщо немає з’єднання з WiFi. + Інфо + Версія додатку + Зворотній зв\'язок + Поділіться ідеями або спитайте про підтримку + Оцініть застосунок + Оцініть Tomahawk в Google Play Крамниці + Відвідайте наш сайт + Відкрити tomahawk-player.org у Вашому оглядачі-тенет. + Відправити логи + Відправити Ваші логи для зневадження + Будь ласка, дайте короткий опис проблеми, яка в Вас виникла й вкажіть Вашу електронну адресу, щоб ми могли законтактувати з Вами. Дякуємо! + Ваша електронна адреса + Помилка + Tomahawk припинив роботу + Несподівана помилка припинила роботу проґрами Будь ласка, допоможіть нам виправити це, відіславши нам дані про помилку, Все, що від Вас потрібно - це натиснути Гаразд. + Ви можете додати нижче Ваші коментарі про проблему: + Дякуємо ! + Відтворити + Додати до Черги + Додати до Грайлиста + Створити станцію + Вилучити + Вилучити із Грайлиста + + Вилучити з колекції + + Додати до колекції + + Улюблене + + Неулюблене + Поділитися + Ви повинні увійти в Hatchet, щоб поділитися цим грайлистом. + Фід + Чарти + Колекція + Грайлисти + Станції + Улюблене + Параметри + Хмарна колекція + Нещодавно Додана + Виконавець від А до Я + Кількість Відтворень + Альбом від А до Я + Від А до Я + Минулопис + Нормально + Добре + Найкраще + Відкрити Панель Навігації + Закрити Панель Навігації + + Читані + + Підписатися + + Раджені люди + + %1$s підписався + + + %1$s вподобав пісню + %1$s вподобані %2$d пісні + %1$s вподобаних %2$d пісень + + + + + + %1$s прокоментував + + + + %1$s створив грайлист + %1$s створив %2$d грайлисти + %1$s створив %2$d грайлистів + + + %1$s від %2$s + кілька секунд тому + + хвилину тому + %1$d хвилини тому + %1$d хвилин тому + + + годину тому + %1$d години тому + %1$d годин тому + + + день тому + %1$d дні тому + %1$d днів тому + + %1$s\'s нещодавно слухав + %1$s\'s Улюблене + %1$s\'s Грайлист + Мої нещодавно прослухані + Моє улюблене + Мій грайлист + Музика + Життєпис + + Схожі + + Пісні + + + %1$d Пісня + %1$d Пісні + %1$d Пісень + + Дія + + Читаючі + + Читані + + Підписувачі: %1$d, Підписки: %2$d + Увімк. + Вимк. + Увійти в + Вийти з + Стягнути втулку для + Бажаєте автоматично зберігати дані про усі доріжки, які відтворюються на Hatchet? + Ми зробимо це для Вас! Будь ласка, увімкніть наш сервіс в налаштуваннях Вашої системи. + Закрити + Переглянути альбом + З’єднатися + Виберіть сервіси, які бажали б додати. Tomahawk працює тим краще, чим більше сервісів активовано. + Встановлено у звичайний спосіб + Розширені + Інфо + Ви можете перемотувати пісню довгим натисканням на кнопку Відтворення. + Натисніть, щоб додати пісню до черги. + Потягніть вгору, щоб побачити поточний грайлист. + Потягніть донизу, щоб закрити вікно відтворення. + Змахніть наліво або направо, щоб увімкнути наступну або попередню пісню. Довге натискання, щоб відкрити меню. Подвійне натискання, щоб додати в улюблене. + + Наступна + Починаючи роботу + Музика всюди.\nЙ тепер Вам не доведеться за нею всюди бігати. + Досить вишукувати музику шастаючи безмежними тенетами всесвітнього павутиння - сервісами, які Ви не використовуєте або до яких не маєте доступу. Лише за ім’ям або посиланням на пісню, альбомом чи грайлистом, Tomahawk знайде пісню у ліпшій якости й відтворить її. + Волієте поспілкуватися? + З Hatchet Ви, нарешті, можете мати свою музичну колекцію зібрану у одному місці. Діліться грайлистами й улюбленими піснями. Або просто синхронізуйте Застосунок з Tomahawk на Вашому комп’ютері. + З’єднатися + Виконано! + Усі завдання виконано! + Веселого Вам використання нашого застосунку й не забудьте сказати, що Ви про нього думаєте! + + Виберіть грайлист, у який потрібно додати %1$d пісню: + Виберіть грайлист, у який потрібно додати %1$d пісні: + Виберіть грайлист, у який потрібно додати %1$d пісень: + + На жаль, цей засіб не підтримує пошук. + Встановлення втулок + Ви бажаєте додати в Tomahawk втулку з невідомого джерела. Це небезпечно. + Ви справді волієте встановити цю Втулку? + Вилучити втулку + Ви справді бажаєте вилучити цю Втулку? + Виберіть обліковку: + Неможливо відкрити файл. + Невідомий альбом + Невідомий виконавець + Не знайдено Додаток Налаштувань на Вашому пристрої. Будь ласка, надайте Tomahawk доступ до сповіщень у Системних Налаштуваннях. + Увага: Ви завантажите втулку з частиною закритого коду. + Потрібна втулка застаріла й більше не сумісна. Ви бажаєте оновити її зараз? + Потрібна втулка не встановлена. Ви волієте встановити її зараз? + + Нове + Призупинити + Грати + Попередній трек + Наступний трек + Додати до улюблених + Вилучити з улюблених + + + Перемішати & Грати + Завантажити станцію: + Пропоновані станції + Мої станції + diff --git a/app/src/main/res/values-uz-rUZ/strings.xml b/app/src/main/res/values-uz-rUZ/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-uz-rUZ/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-v19/bool_switches.xml b/app/src/main/res/values-v19/bool_switches.xml new file mode 100644 index 000000000..908b577a6 --- /dev/null +++ b/app/src/main/res/values-v19/bool_switches.xml @@ -0,0 +1,25 @@ + + + + + true + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..df0e36417 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,250 @@ + + + 确定 + 取消 + 登录 + 注销 + 注册 + 注册 & 登陆 + 立刻重新扫描 + 内部存储 + 曲目 + 专辑 + 艺术家 + 用户 + 热单 + 热门专辑 + 专辑和EP + 其他发行版 + 播放列表详情 + 专辑详情 + 搜索 + 播放列表 + 随机开 + 随机关 + 重复开 + 重复关 + 未知 + 无曲目 + 保存当前播放列表 + 创建播放列表 + + 名称 + 创建电台 + 搜索艺术家/歌曲/风格 + 风格 + 情绪 + 邮箱/用户名 + 用户名 + 密码 + 密码确认 + 邮箱(可选) + 此项必填 + 密码不匹配 + 用户名或密码不正确 + 账户被服务器拒绝 + 无法认证。请检查您的连接 + 操作不允许,账户已在别处使用 + 您的账户已过期 + + 已登录到 %1$s + + 已从 %1$s 注销 + 本地收藏 + 本地 + 所有 + 请选择需要扫描的本地文件夹 + 使用 %1$s 同步数据 + 回放 + 均衡器 + 自定义音频输出。 + 收藏所有内容 + 将手机中所有音乐播放软件的曲目收藏到 %1$s + 即插即播 + 只要插入耳机就开始播放音乐 + 优先采用音质 + 如果未连接到 WiFi 优先使用的音质 + 信息 + 软件版本 + 反馈 + 分享您的想法或获取帮助 + 评价此APP + 在 Google Play 商店对 Tomahawk 评分。 + 访问我们的网站 + 在浏览器中打开 tomahawk-player.org 。 + 发送日志 + 发送日志,用于诊断。 + 请简单描述您遇到的问题并留下您的邮箱,以便我们想更进一步了解问题信息时联系您。 + 邮箱地址 + 问题描述 + Tomahawk 已崩溃 + 因为不可意料的错误导致程序停止运行。请通过发送错误信息帮助我们修复此问题,您只需点击确定即可 + 你可以在下面添加关于问题的描述: + 感谢您! + 播放 + 添加到队列 + 添加到播放列表 + 创建电台 + 删除 + 从播放列表中移除 + + 从收藏中移除 + + 添加到收藏 + + 喜欢 + + 不喜欢 + 分享 + 你必须登陆到 Hatchet 才能分享此播放列表。 + 订阅 + 榜单 + 收藏 + 播放列表 + 电台 + 收藏 + 设置 + 云收藏 + 最近添加 + 艺术家 A-Z + 播发统计 + 专辑 A-Z + A-Z + 历史 + 正常 + + 最佳 + 打开导航抽屉 + 关闭导航抽屉 + + 正在关注 + + 关注 + + 推荐的关注 + + %1$s 正在关注 + + + %1$s 喜欢了 %2$d 个曲目 + + + + %1$s 收藏了 %2$d 个曲目 + + + + %1$s 收藏了%2$d 张专辑 + + + + %1$s 收藏了%2$d 个艺术家 + + + %1$s 评论了 + + + %1$s 跟随收听 %2$d 个好友 + + + + %1$s 创建了 %2$d 个播放列表 + + + %1$s - %2$s + 几秒前 + + %1$d 分钟前 + + + %1$d 小时前 + + + %1$d 天前 + + %1$s 最近播放的 + %1$s 的收藏 + %1$s 的播放列表 + 最近播放的 + 我的收藏 + 我的播放列表 + 音乐 + 简介 + + 类似的 + + 歌曲 + + + %1$d 首歌 + + 活动 + + 跟随者 + + 在关注 + + 粉丝:%1$d,关注:%2$d + 启用 + 禁用 + 登录到 + 注销 + 下载插件: + 自动保存您在 Hatchet 上播放的曲目的数据? + 只需要在系统设置中启用此服务 + 关闭 + 查看专辑 + 连接 + 请选择要使用的服务。激活的服务更多,Tomahawk 工作的越好。 + 手动安装的 + 高级 + 信息 + 你可以长按播放键来快进/快退。 + 轻击添加曲目到播放队列。 + 上拉显示当前播放列表。 + 下拉关闭播放控制栏。 + 左/右滑跳到上首/下首歌曲。长按打开文本菜单。双击添加到收藏。 + + 下一首 + 入门指南 + 到处都是音乐。\n 现在一切都不必如此。 + 不用再为找音乐资源头疼。只需指定歌名,专辑名或者播放列表,Tomahawk 会自动搜索最佳的源并播放 + 需要社交? + Hatchet 在手,再多音乐收藏也不愁。分享,收藏,手机桌面同步。 + 连接 + 完成! + 全部完成! + 祝您探索愉快,别忘了给我们反馈哦! + + 将 %1$d 添加到播放列表: + + 抱歉,此解析器不支持寻位。 + 插件安装 + 将会从未知源安装 Tomahawk 插件。此操作存在安全风险。 + 您是否想安装此插件? + 卸载插件 + 您是否想卸载此插件? + 请选择一个账号: + 无法打开文件 + 未知专辑 + 未知艺术家 + 无法在你的设备上找到设置应用。请通过系统设置允许 Tomahawk 访问你的通知。 + 警告:你即将下载包含闭源组件的插件。 + 一个必需的插件应用已过期且不再兼容。你希望更新这个插件吗? + 一个必需的插件应用未安装。你希望安装这个插件吗? + + 新建 + 暂停 + 播放 + 上个曲目 + 下个曲目 + 添加到收藏 + 从收藏中移除 + + 正在播放 + + 随机播放 + 载入电台中: + 推荐的电台 + 我的电台 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..d3905629b --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..5bad76fed --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,27 @@ + + + + + @string/preferences_bitrate_low + @string/preferences_bitrate_medium + @string/preferences_bitrate_high + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 000000000..8972fccb8 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/bool_switches.xml b/app/src/main/res/values/bool_switches.xml new file mode 100644 index 000000000..c3a53bea2 --- /dev/null +++ b/app/src/main/res/values/bool_switches.xml @@ -0,0 +1,25 @@ + + + + + false + + \ No newline at end of file diff --git a/app/src/main/res/values/colors_tomahawk.xml b/app/src/main/res/values/colors_tomahawk.xml new file mode 100644 index 000000000..e62ea75db --- /dev/null +++ b/app/src/main/res/values/colors_tomahawk.xml @@ -0,0 +1,59 @@ + + + + + #FFff004c + #DDff004c + #44ff004c + #3B3F45 + #FFffffff + #FF777777 + #FF0f0f11 + #FF333333 + #EEffffff + #99151515 + #99000000 + #AA000000 + #55000000 + #333 + #BB000000 + #CC000000 + #FF999999 + #55FFFFFF + #FF222222 + #FF777777 + #FFffffff + #FF777777 + #FFcccccc + #FFe8837c + #FFf1f1f1 + #88cbcbcb + #11000000 + #151515 + + #3e454b + #84bd00 + #3892e3 + #993892e3 + #22222c + #FF999999 + + \ No newline at end of file diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml new file mode 100644 index 000000000..6ad9b668d --- /dev/null +++ b/app/src/main/res/values/constants.xml @@ -0,0 +1,9 @@ + + + + false + 2 + 3 + false + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..efc109df0 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,67 @@ + + + + 2dp + 4dp + 8dp + 12dp + 16dp + 24dp + 32dp + 48dp + + 16dp + 32dp + 48dp + 64dp + + 32dp + 48dp + + 32dp + + 24dp + + 48dp + 16dp + + 48dp + 104dp + 144dp + 120dp + 64dp + 180dp + 208dp + 48dp + 32dp + + 2dp + 5dp + + 12sp + 14sp + 16sp + 18sp + 22sp + + 32dp + + 32dp + 48dp + + 56dp + 38dp + 16dp + + 64dp + 128dp + 12dp + 48dp + 48dp + 24dp + + 16dp + + 48dp + + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 000000000..30ce1318f --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..b6a3b4274 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,263 @@ + + + Tomahawk + OK + Cancel + Login + Logout + Register + Register & Login + Rescan now + Internal Storage + Tracks + Albums + Artists + Users + Top Hits + Top Albums + Albums and EPs + Other releases + Playlist Details + Album Details + Search + Playlist + Shuffle is on + Shuffle is off + Repeat is on + Repeat is off + unknown + no track + Save the current playlist + Create playlist + + Title + Create station + Search for artists/songs/genres + Genres + Moods + E-Mail/Username + Username + Password + Password confirmation + E-Mail (optional) + This field is required + Passwords don\'t match + Username or password incorrect. + Account rejected by server. + Unable to authenticate. Please check your connection. + Action not allowed, account is in use elsewhere. + Your account has expired. + + Logged into %1$s + + Logged out of %1$s + Local Collection + Local + All + Choose the directories which should be scanned for local media. + Sync your data with %1$s. + Playback + Equalizer + Customize the audio output. + Scrobble Everything + Scrobble every track from every music player app on the phone to %1$s. + Plug In to Play + Starts playing music as soon as a headset is connected. + Preferred Audio Quality + The preferred audio quality if not connected to WiFi. + Info + App-Version + Feedback + Share your ideas or ask for support. + Rate the App + Rate Tomahawk on the Google Play Store. + Visit our Website + Open tomahawk-player.org in your browser. + Send Log + Send your log file for debug purposes. + Please briefly describe the issue you are having and provide an email so we can contact you if needed. Thanks! + Your email address + Issue + Tomahawk has crashed + An unexpected error occurred forcing the application to stop. Please help us fix this by sending us error data, all you have to do is click OK. + You might add your comments about the problem below: + Thank you ! + Play + Add to Queue + Add to Playlist + Create Station + Delete + Remove from Playlist + + Remove from Collection + + Add to Collection + + Love + + Unlove + Share + You have to log into Hatchet to be able to share this playlist. + Feed + Charts + Collection + Playlists + Stations + Favorites + Settings + Cloud Collections + Recently Added + Artist A-Z + Play Count + Album A-Z + A-Z + History + Normal + Good + Best + Open Navigation Drawer + Close Navigation Drawer + + Following + + Follow + + Suggested people to follow + + %1$s is now following + + + %1$s loved a track + %1$s loved %2$d tracks + + + + %1$s collected a track + %1$s collected %2$d tracks + + + + %1$s collected an album + %1$s collected %2$d albums + + + + %1$s collected an artist + %1$s collected %2$d artists + + + %1$s commented on + + + %1$s listened along to a friend + %1$s listened along to %2$d friends + + + + %1$s created a playlist + %1$s created %2$d playlists + + + %1$s by %2$s + a few seconds ago + + a minute ago + %1$d minutes ago + + + an hour ago + %1$d hours ago + + + a day ago + %1$d days ago + + %1$s\'s recently played + %1$s\'s Favorites + %1$s\'s Playlist + My recently played + My Favorites + My Playlist + Music + Biography + + Similar + + Songs + + + %1$d Song + %1$d Songs + + Activity + + Followers + + Following + + Followers: %1$d, Following: %2$d + Enable + Disable + Log into + Log out of + Download Plugin for + Want to automatically save data about all the tracks you play on Hatchet? + We\'ll do that for you! Please enable our service in your system settings. + Close + View Album + Hatchet + Connect + Select the services you want to use. Tomahawk works better when more services are activated. + Manually installed + Advanced + Info + You can seek through a track with a long-press on the Play button. + Tap to add the track to the queue. + Pull up to see the current playlist. + Pull down to close the playback drawer. + Swipe left/right to skip to the next/previous track. Long-press to open context menu. Double-tap to add to favorites. + + Next + Getting started + Music is everywhere.\nNow you don\'t have to be. + Stop chasing music across the web - to services you don\'t use, or sources you don\'t have access to. Given the name or link to a song, album or playlist, Tomahawk will find the best available source for you and just play it. + Want to socialize? + With Hatchet you can finally have your music collection in one place. Share your playlists and favorite tracks. Or simply sync the App with Tomahawk on your desktop machine. + Connect + Done! + All done! + Have fun exploring the app and be sure to tell us what you think! + + Choose a playlist to add %1$d track to: + Choose a playlist to add %1$d tracks to: + + Sorry, this resolver doesn\'t support seeking. + Plugin installation + You are about to install a Tomahawk plugin from an unknown source. This can be a security risk. + Do you really want to install this Plugin? + Uninstalling plugin + Do you really want to uninstall this Plugin? + Choose an account: + Can\'t open file. + Unknown album + Unknown artist + Couldn\'t find Settings App on your device. Please give Tomahawk access to your notifications in your System Settings. + Warning: You are about to download a plugin that contains closed-source parts. + The required plugin application is outdated and no longer compatible. Do you want to update it now? + The required plugin application is no longer installed. Do you want to install it now? + + new + Pause + Play + Previous Track + Next Track + Add to Favorites + Remove from Favorites + + Now Playing + + Shuffle & Play + Loading station: + Suggested Stations + My Stations + diff --git a/app/src/main/res/values/styles_tomahawk.xml b/app/src/main/res/values/styles_tomahawk.xml new file mode 100644 index 000000000..3dcea8bc9 --- /dev/null +++ b/app/src/main/res/values/styles_tomahawk.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes_tomahawk.xml b/app/src/main/res/values/themes_tomahawk.xml new file mode 100644 index 000000000..93cc7b4b3 --- /dev/null +++ b/app/src/main/res/values/themes_tomahawk.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/allowed_media_browser_callers.xml b/app/src/main/res/xml/allowed_media_browser_callers.xml new file mode 100644 index 000000000..6649d8d89 --- /dev/null +++ b/app/src/main/res/xml/allowed_media_browser_callers.xml @@ -0,0 +1,238 @@ + + + + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIDvTCCAqWgAwIBAgIJAOfkBvDXw5bzMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwMjUxWhcNNDExMDEyMjMwMjUxWjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAou7wwBKFyznqpRretJ3EVp55/Yr049Ag5wlGvrCnjIP8DrMrU+skfKe1 + DmwpsLNtnhhiNH+J000Lok3hc8jdWKeKOopzKGDNvL/HvnS70Zyk26gj9jtMMHz9 + 2aZdpmwD67FNmTlG2FERr+TwMD5agaPnsFR2zla6ugUvHGzz65YDxpCZsQ/TowyD + LnxgMagvhvS+Oex3yh2FN7pJfwS03KdGdkWPbLqf9Fem09s5jjeZW/O3RgnKoRPI + J4QLK70efjAZqJyBGcDZyQMwOs+8HIknraf8+cRZJDzqOx7rttl8M3KGB2EFljTp + 6/FyxJLnAo6QlXn7GrYalTI0yLU9dQIDAQABo1AwTjAdBgNVHQ4EFgQU9QPJ5xJE + DA8MDQMrj0hm2/A2BRkwHwYDVR0jBBgwFoAU9QPJ5xJEDA8MDQMrj0hm2/A2BRkw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEADcr5h1FR8IpmN4hSsUA9 + SnCQVyXa1GQhzpQgRbF+npkgOn2Mebp8bd28VpfgooD2OBNQXCUcZkn7pWj++ut9 + HhObHVaV5FNg0pdDqLna9QZ9Y4oS+ZrijK70XZ/EjlYUHvhu0pIjZAbD8CmCFlow + SR55qCSjM5iS37LZB32SMr1BBiYrNAvncKjYQVK8ctTRzhpNQQPBgXBA98Xl+d1D + Py00JWQuF0ssmhKcJuvfdEnFF7Hvaxz/gCQ9nzarQI3CJB8dOXVwF8mcyDRBz4JR + +YDpXo6BD+fGt15ov+zmqC8xaT9P1/JgoDXiMhy/6rwgdi9WxPf8mb7TnBC+CksX + 0A== + + + MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD + Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+ + 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM + oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm + k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6 + 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu + 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/ + wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m + 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9 + QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC + 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/ + yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W + IQ== + + + MIIDvzCCAqegAwIBAgIJANqYw9kVc9PvMA0GCSqGSIb3DQEBBQUAMHYxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDESMBAG + A1UEAwwJQ2xvY2tXb3JrMB4XDTE0MDMwNzIyMDExOFoXDTQxMDcyMzIyMDExOFow + djELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1v + dW50YWluIFZpZXcxFDASBgNVBAoMC0dvb2dsZSBJbmMuMRAwDgYDVQQLDAdBbmRy + b2lkMRIwEAYDVQQDDAlDbG9ja1dvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw + ggEKAoIBAQDcHW9LKO04MBSynIL22v/THd57jB5jCEBlC1ixZaNqrrYscVOVLgRF + Ca+CH5S6n08YZMOntdZTzAAVnQAQ4eVm+jeq/xg2Xa57SoXdsfODzEdwoj6VYpH+ + tXLBaTFar0706qWuhh/N1ufl6tQxE3RGRgx8KPsyLJKVXFx6qJV3w3A/l+CYt362 + oG6sa3LqoK0hCrAqH9z8dmJ0dEGpPzzqihb0jJciweMyQTJ+wsn3MDEujRvv7ikL + RRo0iSys71sUctbZfvlUKMyK1e8EuMTx9Q3SQtVdclhmhVBbXksbHlmtjB2FL6CC + SBVnO8bmQynsxOrU24RkqWsxg5+f28kHAgMBAAGjUDBOMB0GA1UdDgQWBBSEhUcQ + hKQ8s+r4P6shYqCVAM4sejAfBgNVHSMEGDAWgBSEhUcQhKQ8s+r4P6shYqCVAM4s + ejAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAHOWILon0qD1SIQ60b + YI4cKdkBKIHq/D/WKF9fYmqXPvBX5pfusqxcouYFyj0z9ZCZa4sAMsRH5lAPJb0X + yvmVAzmDQMjubNy1O+3ksfJI59AgmZ6B58rqpTLP2pn+SqXtQEBORPdb79J/yts1 + uLIblHhGXhci8nr7KwtuFY5ExKsMT2V7Gdd9j1PJz7nuLU9FtlTgEryN6YHkwuLD + 055RkwPYrk0swchijXhXrnU/HXsCo6cFeMYF4wUcbB2pSRrOE7uI0H2BfdSUJlGX + hK6WlaRHNQ2J60BekPKr82auL8pY0va/Hb9LHEie4KABVN/PAiUS9aHHIp5zHePw + R9b4 + + + MIIFYTCCA0sCBgFEnpGW0zALBgkqhkiG9w0BAQUwdjELMAkGA1UEBhMCVVMxEzAR + BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNV + BAoTC0dvb2dsZSBJbmMuMRAwDgYDVQQLEwdBbmRyb2lkMRIwEAYDVQQDEwlDbG9j + a1dvcmswHhcNMTQwMzA3MjIwMjI1WhcNMzgwMTE5MDMxNDA3WjB2MQswCQYDVQQG + EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll + dzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEjAQBgNV + BAMTCUNsb2NrV29yazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALo8 + fzkL/lmKYrz8izyUxidamRXt3N03OlVqmQvi/UP3nxizAdJAJ+NyfwnO/eKcfCaw + iiDeNn0a3+NOp4+uN/OQ9eAmcnpOCCg773b49kO8FSc2oEg/ybeRq1I8872Ge2Yt + KidMANiQ550R6LAmX+2pddzI5UKZiY6QE2picYwuCy85eVHbJLFXob/nxWvOSjgL + Jfq5JmM+qJQEOOC2lAu5nol+LvoFPDIpm9lhb6S9loIhezdDH83Ygu0hp/LwRn/g + lRy8Wphi40oVa+FaF/8CF5hkRNYTsR7XX4OAGO60/ZTkj1rjHOSvpDY4tpcshVzS + 2woBxDJhKOTFGXq+rMxtwuitpEJfD5DVpaVYJGG/eBHhLs4O6gYDP5ZUOe3gcf/E + bCDy374jIzp/ZMHOCa2hy85r9ryiLpuYnErAyWqdbHVP7Bhx2HsQmMGg3mC8fXfe + MNVOuEfOaxJ8GR6nk28KRsFG8za5NOq6Wl8cA2S3UpZVRDJQ/WOq5Xvrq+AmPwkI + TRlEBgw62bu6f3n03jwrLTe6sw1LuRHcUWngr5VS9NOOPbPyy4AcUgJScGG/AbBC + 0H6J5I8RqaqgJ/BElZ7aKMXd2FNXpx45u4JRs1frb3IY/MwXGIGmMGdGMeBVlDka + emea8lqgYgHWIrjQCd0R1QaiAw8kJ65n2Bs0N3l5AgMBAAEwCwYJKoZIhvcNAQEF + A4ICAQCkxJaWNGHIlTWlsQrNASQ3aonaJ0OdrDADSGcLICut4z8vuioHZAO1C+hx + yiqym769u8QG2wk5QcmMF2oORv+Q/wAWFgREgG7cguEw/hCGHuMFnbd+PZ2poq00 + qdK02hsm/VpbcBzVbP7pZHrkFDuXpnwCgGWxf54U8jKl7xfhZKFJF5KWlBwwvVo2 + q/jzQsjjr7xvSUNzB31qnBHXOSINWte1GS+bHP6Wj0pysbhUdeDpiL5ocohmZbEr + 9O7DIlZU9eHyK4vrVY6+ZneL1l8JkS35XoCd/u8Px+rKXQ6+HUEEH+cgyzKbMH45 + LhOX8SA5VGkwhIt/AhdAiS32x6By5984usPXIjVv5lR/anxXit9nyT0rNYiTVDXw + +aETzi3szW2hncNLQYLsrtYg61KFMCXF4ATstFG8ReFIWsw2f7ZJkq9ZTFUbC8k4 + y9Ya1WdZkCD3OmXhqcikiNusgx7rkY8MkikJXt5BBXs8rupOFsW5RUuS4lmKEbSU + oh8/er+DwGf0GC6YQZuk5JOKNIDwhi/tr1dySlUzV4/aX7PN/PlUgH//2MlRd+d1 + BKZCvlzboOEAZfx8aBKc7SezqATXpM3ZDNPsywWoyIpgmtBWoE60ih4Flf05XB+n + e7MdpSQ0Xgq9TgG1BoJP6rpC0y3Ukmc+z8AXnYYdJunNXEbv0A== + + + MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD + Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+ + 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM + oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm + k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6 + 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu + 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/ + wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m + 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9 + QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC + 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/ + yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W + IQ== + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + diff --git a/res/xml/authenticator.xml b/app/src/main/res/xml/authenticator.xml similarity index 78% rename from res/xml/authenticator.xml rename to app/src/main/res/xml/authenticator.xml index ab7590078..aa077a727 100644 --- a/res/xml/authenticator.xml +++ b/app/src/main/res/xml/authenticator.xml @@ -19,9 +19,7 @@ */ --> + android:accountType="is.hatchet.account" + android:icon="@drawable/ic_hatchet" + android:smallIcon="@drawable/ic_hatchet" + android:label="@string/authenticator_label"/> diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 000000000..b572dfe8b --- /dev/null +++ b/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/assets/js/exfm/exfm-icon.png b/assets/js/exfm/exfm-icon.png deleted file mode 100644 index e8556d153..000000000 Binary files a/assets/js/exfm/exfm-icon.png and /dev/null differ diff --git a/assets/js/exfm/exfm.js b/assets/js/exfm/exfm.js deleted file mode 100644 index a129bd99b..000000000 --- a/assets/js/exfm/exfm.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * (c) 2011 lasconic - */ -var ExfmResolver = Tomahawk.extend(TomahawkResolver, { - settings: { - name: 'Ex.fm', - icon: 'exfm-icon.png', - weight: 30, - timeout: 5 - }, - - resolve: function (qid, artist, album, title) { - // build query to 4shared - var url = "http://ex.fm/api/v3/song/search/"; - - url += encodeURIComponent(title); - - url += "?start=0&results=20"; - - // send request and parse it into javascript - var that = this; - var xmlString = Tomahawk.asyncRequest(url, function(xhr) { - // parse json - var response = JSON.parse(xhr.responseText); - - var results = new Array(); - - // check the response - if (response.results > 0) { - var songs = response.songs; - - // walk through the results and store it in 'results' - for (var i = 0; i < songs.length; i++) { - var song = songs[i]; - var result = new Object(); - if(song.url.indexOf("http://api.soundcloud") === 0){ // unauthorised, use soundcloud resolver instead - continue; - } - - if (song.artist !== null){ - - if (song.title !== null){ - - var dTitle = ""; - if (song.title.indexOf("\n") !== -1){ - var stringArray = song.title.split("\n"); - var newTitle = ""; - for (var j = 0; j < stringArray.length; j++){ - newTitle += stringArray[j].trim() + " "; - } - dTitle = newTitle.trim(); - } - else { - dTitle = song.title; - } - - dTitle = dTitle.replace("\u2013","").replace(" ", " ").replace("\u201c","").replace("\u201d",""); - if (dTitle.toLowerCase().indexOf(song.artist.toLowerCase() + " -") === 0){ - dTitle = dTitle.slice(song.artist.length + 2).trim(); - } - else if (dTitle.toLowerCase().indexOf(song.artist.toLowerCase() + "-") === 0){ - dTitle = dTitle.slice(song.artist.length + 1).trim(); - } - else if (dTitle.toLowerCase() === song.artist.toLowerCase()){ - continue; - } - else if (dTitle.toLowerCase().indexOf(song.artist.toLowerCase()) === 0){ - dTitle = dTitle.slice(song.artist.length).trim(); - } - var dArtist = song.artist; - } - } - else { - continue; - } - if (song.album !== null){ - var dAlbum = song.album; - } - if (dTitle.toLowerCase().indexOf(title.toLowerCase()) !== -1 && dArtist.toLowerCase().indexOf(artist.toLowerCase()) !== -1 || artist === "" && album === ""){ - result.artist = ((dArtist !== "")? dArtist:artist); - result.album = ((dAlbum !== "")? dAlbum:album); - result.track = ((dTitle !== "")? dTitle:title); - result.source = that.settings.name; - result.url = song.url; - result.extension = "mp3"; - result.score = 0.80; - results.push(result); - } - if (artist !== "") { // resolve, return only one result - break; - } - } - } - - var return1 = { - qid: qid, - results: results - }; - Tomahawk.addTrackResults(return1); - }); - }, - - search: function (qid, searchString) { - this.settings.strictMatch = false; - this.resolve(qid, "", "", searchString); - } -}); - -Tomahawk.resolver.instance = ExfmResolver; diff --git a/assets/js/jamendo/jamendo-icon.png b/assets/js/jamendo/jamendo-icon.png deleted file mode 100644 index e730436cb..000000000 Binary files a/assets/js/jamendo/jamendo-icon.png and /dev/null differ diff --git a/assets/js/jamendo/jamendo-resolver.js b/assets/js/jamendo/jamendo-resolver.js deleted file mode 100644 index ec1d586d7..000000000 --- a/assets/js/jamendo/jamendo-resolver.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * (c) 2011 lasconic - */ -var JamendoResolver = Tomahawk.extend(TomahawkResolver, { - settings: { - name: 'Jamendo', - icon: 'jamendo-icon.png', - weight: 75, - timeout: 5 - }, - handleResponse: function(qid, xhr) { - // parse xml - var domParser = new DOMParser(); - xmlDoc = domParser.parseFromString(xhr.responseText, "text/xml"); - - var results = new Array(); - var r = xmlDoc.getElementsByTagName("data"); - // check the response - if (r.length > 0 && r[0].childNodes.length > 0) { - var links = xmlDoc.getElementsByTagName("track"); - - // walk through the results and store it in 'results' - for (var i = 0; i < links.length; i++) { - var link = links[i]; - - var result = new Object(); - result.artist = Tomahawk.valueForSubNode(link, "artist_name"); - result.album = Tomahawk.valueForSubNode(link, "album_name"); - result.track = Tomahawk.valueForSubNode(link, "name"); - //result.year = Tomahawk.valueForSubNode(link, "year"); - result.source = this.settings.name; - result.url = decodeURI(Tomahawk.valueForSubNode(link, "stream")); - // jamendo also provide ogg ? - result.extension = "mp3"; - //result.bitrate = Tomahawk.valueForSubNode(link, "bitrate")/1000; - result.duration = Tomahawk.valueForSubNode(link, "duration"); - result.score = 1.0; - - results.push(result); - } - } - var return1 = { - qid: qid, - results: results - }; - Tomahawk.addTrackResults(return1); - }, - - sendRequest: function (url) { - // send request and parse it into javascript - Tomahawk.asyncRequest(url, this.handleResponse); - }, - resolve: function (qid, artist, album, title) { - // build query to Jamendo - var url = "http://api.jamendo.com/get2/id+name+duration+stream+album_name+artist_name/track/xml/track_album+album_artist/?"; - if (title !== "") url += "name=" + encodeURIComponent(title) + "&"; - - if (artist !== "") url += "artist_name=" + encodeURIComponent(artist) + "&"; - - if (album !== "") url += "album_name=" + encodeURIComponent(album) + "&"; - - url += "n=20"; - - var that = this; - Tomahawk.asyncRequest(url, function(xhr) { that.handleResponse(qid, xhr); } ); - }, - search: function (qid, searchString) { - // build query to Jamendo - var url = "http://api.jamendo.com/get2/id+name+duration+stream+album_name+artist_name/track/xml/track_album+album_artist/?"; - if (searchString !== "") url += "searchquery=" + encodeURIComponent(searchString); - - url += "&n=20"; - var that = this; - Tomahawk.asyncRequest(url, function(xhr) { that.handleResponse(qid, xhr); } ); - }, -}); - -Tomahawk.resolver.instance = JamendoResolver; \ No newline at end of file diff --git a/assets/js/official.fm/officialfm-icon.png b/assets/js/official.fm/officialfm-icon.png deleted file mode 100644 index e9f01d6a0..000000000 Binary files a/assets/js/official.fm/officialfm-icon.png and /dev/null differ diff --git a/assets/js/official.fm/officialfm.js b/assets/js/official.fm/officialfm.js deleted file mode 100644 index f86ce0b43..000000000 --- a/assets/js/official.fm/officialfm.js +++ /dev/null @@ -1,121 +0,0 @@ -var OfficialfmResolver = Tomahawk.extend(TomahawkResolver, { - settings: { - name: 'Official.fm', - icon: 'officialfm-icon.png', - weight: 70, - timeout: 5 - }, - - spell: function(a){magic=function(b){return(b=(b)?b:this).split("").map(function(d){if(!d.match(/[A-Za-z]/)){return d}c=d.charCodeAt(0)>=96;k=(d.toLowerCase().charCodeAt(0)-96+12)%26+1;return String.fromCharCode(k+(c?96:64))}).join("")};return magic(a)}, - - init: function () { - this.secret = this.spell("yptuKlFHC3azLLcBNYoCHW6t30I1M5uy"); - }, - - asyncRequest: function (url, callback) { - var xmlHttpRequest = new XMLHttpRequest(); - xmlHttpRequest.open('GET', url, true); - xmlHttpRequest.setRequestHeader('X-Api-Version', 2.0); - Tomahawk.log("Doing API call: " + url); - xmlHttpRequest.onreadystatechange = function () { - if (xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200) { - callback.call(window, xmlHttpRequest); - } else if (xmlHttpRequest.readyState === 4) { - Tomahawk.log("Failed to do GET request: to: " + url); - Tomahawk.log("Status Code was: " + xmlHttpRequest.status); - } - } - xmlHttpRequest.send(null); - }, - - resolve: function (qid, artist, album, title) { - if (artist !== "") { - query = encodeURIComponent(artist) + "+"; - } - if (title !== "") { - query += encodeURIComponent(title); - } - var apiQuery = "http://api.official.fm/tracks/search?api_key=" + this.secret + "&fields=streaming&api_version=2.0&q=" + query; - var that = this; - var resultObj = { - results: [], - qid: qid - }; - that.asyncRequest(apiQuery, function (xhr) { - var resp = JSON.parse(xhr.responseText); - if (resp.total_entries !== 0) { - for (var i = 0; i < Math.min(3, resp.total_entries); i++) { - if (resp.tracks[i] === undefined || resp.tracks[i].track === undefined) { - continue; - } - var track = resp.tracks[i].track; - - Tomahawk.log("Result: " + JSON.stringify(track)); - if (track.streaming === undefined || track.streaming.http === undefined) { - Tomahawk.log("Found result from Official.fm but no streaming url..."); - continue; - } - - var result = { - track: track.title, - artist: track.artist - }; - - result.source = that.settings.name; - result.mimetype = "audio/mpeg"; - result.bitrate = 160; - result.duration = track.duration; - result.score = 0.85; - result.url = track.streaming.http; - - resultObj.results.push(result); - } - } - Tomahawk.addTrackResults(resultObj); - }); - }, - - search: function (qid, searchString) { - var apiQuery = "http://api.official.fm/tracks/search?api_key=" + this.secret + "&api_version=2.0&fields=streaming&q=" + encodeURIComponent(searchString.replace('"', '').replace("'", "")); - var that = this; - var resultObj = { - results: [], - qid: qid - }; - this.asyncRequest(apiQuery, function (xhr) { - var resp = JSON.parse(xhr.responseText); - if (resp.total_entries !== 0) { - for (var i = 0; i < resp.total_entries; i++) { - if (resp.tracks[i] === undefined || resp.tracks[i].track === undefined) { - continue; - } - var track = resp.tracks[i].track; - - Tomahawk.log("Result: " + JSON.stringify(track)); - if (track.streaming === undefined || track.streaming.http === undefined) { - Tomahawk.log("Found result from Official.fm but no streaming url..."); - continue; - } - - var result = { - track: track.title, - artist: track.artist - }; - - result.source = that.settings.name; - result.mimetype = "audio/mpeg"; - result.bitrate = 160; - result.duration = track.duration; - result.score = 0.85; - result.url = track.streaming.http; - - resultObj.results.push(result); - } - } - Tomahawk.addTrackResults(resultObj); - - }); - } -}); - -Tomahawk.resolver.instance = OfficialfmResolver; \ No newline at end of file diff --git a/assets/js/soundcloud/soundcloud-icon.png b/assets/js/soundcloud/soundcloud-icon.png deleted file mode 100644 index 71c74c254..000000000 Binary files a/assets/js/soundcloud/soundcloud-icon.png and /dev/null differ diff --git a/assets/js/soundcloud/soundcloud.js b/assets/js/soundcloud/soundcloud.js deleted file mode 100644 index c5600eb0d..000000000 --- a/assets/js/soundcloud/soundcloud.js +++ /dev/null @@ -1,270 +0,0 @@ -/* - * (c) 2012 thierry göckel - */ -var SoundcloudResolver = Tomahawk.extend(TomahawkResolver, { - - getConfigUi: function () { - var uiData = Tomahawk.readBase64("config.ui"); - return { - "widget": uiData, - fields: [{ - name: "includeCovers", - widget: "covers", - property: "checked" - }, { - name: "includeRemixes", - widget: "remixes", - property: "checked" - }, { - name: "includeLive", - widget: "live", - property: "checked" - }], - images: [{ - "soundcloud.png" : Tomahawk.readBase64("soundcloud.png") - }] - }; - }, - - newConfigSaved: function () { - var userConfig = this.getUserConfig(); - if ((userConfig.includeCovers != this.includeCovers) || (userConfig.includeRemixes != this.includeRemixes) || (userConfig.includeLive != this.includeLive)) { - this.includeCovers = userConfig.includeCovers; - this.includeRemixes = userConfig.includeRemixes; - this.includeLive = userConfig.includeLive; - this.saveUserConfig(); - } - }, - - settings: { - name: 'SoundCloud', - icon: 'soundcloud-icon.png', - weight: 85, - timeout: 15 - }, - - init: function() { - // Set userConfig here - var userConfig = this.getUserConfig(); - if ( userConfig !== undefined ){ - this.includeCovers = userConfig.includeCovers; - this.includeRemixes = userConfig.includeRemixes; - this.includeLive = userConfig.includeLive; - } - else { - this.includeCovers = false; - this.includeRemixes = false; - this.includeLive = false; - } - - - String.prototype.capitalize = function(){ - return this.replace( /(^|\s)([a-z])/g , function(m,p1,p2){ return p1+p2.toUpperCase(); } ); - }; - }, - - getTrack: function (trackTitle, origTitle) { - if ((this.includeCovers === false || this.includeCovers === undefined) && trackTitle.search(/cover/i) !== -1 && origTitle.search(/cover/i) === -1){ - return null; - } - if ((this.includeRemixes === false || this.includeRemixes === undefined) && trackTitle.search(/(re)*mix/i) !== -1 && origTitle.search(/(re)*mix/i) === -1){ - return null; - } - if ((this.includeLive === false || this.includeLive === undefined) && trackTitle.search(/live/i) !== -1 && origTitle.search(/live/i) === -1){ - return null; - } - else { - return trackTitle; - } - }, - - resolve: function (qid, artist, album, title) - { - if (artist !== "") { - query = encodeURIComponent(artist) + "+"; - } - if (title !== "") { - query += encodeURIComponent(title); - } - var apiQuery = "http://api.soundcloud.com/tracks.json?consumer_key=TiNg2DRYhBnp01DA3zNag&filter=streamable&q=" + query; - var that = this; - var empty = { - results: [], - qid: qid - }; - Tomahawk.asyncRequest(apiQuery, function (xhr) { - var resp = JSON.parse(xhr.responseText); - if (resp.length !== 0){ - var results = []; - for (i = 0; i < resp.length; i++) { - // Need some more validation here - // This doesnt help it seems, or it just throws the error anyhow, and skips? - if (resp[i] === undefined){ - continue; - } - - if (!resp[i].streamable){ // Check for streamable tracks only - continue; - } - - // Check whether the artist and title (if set) are in the returned title, discard otherwise - // But also, the artist could be the username - if (resp[i].title !== undefined && (resp[i].title.toLowerCase().indexOf(artist.toLowerCase()) === -1 || resp[i].title.toLowerCase().indexOf(title.toLowerCase()) === -1)) { - continue; - } - var result = new Object(); - result.artist = artist; - if (that.getTrack(resp[i].title, title)){ - result.track = title; - } - else { - continue; - } - - result.source = that.settings.name; - result.mimetype = "audio/mpeg"; - result.bitrate = 128; - result.duration = resp[i].duration / 1000; - result.score = 0.85; - result.year = resp[i].release_year; - result.url = resp[i].stream_url + ".json?client_id=TiNg2DRYhBnp01DA3zNag"; - if (resp[i].permalink_url !== undefined) result.linkUrl = resp[i].permalink_url; - results.push(result); - } - var return1 = { - qid: qid, - results: [results[0]] - }; - Tomahawk.addTrackResults(return1); - } - else { - Tomahawk.addTrackResults(empty); - } - }); - }, - - search: function (qid, searchString) - { - var apiQuery = "http://api.soundcloud.com/tracks.json?consumer_key=TiNg2DRYhBnp01DA3zNag&filter=streamable&q=" + encodeURIComponent(searchString.replace('"', '').replace("'", "")); - var that = this; - var empty = { - results: [], - qid: qid - }; - Tomahawk.asyncRequest(apiQuery, function (xhr) { - var resp = JSON.parse(xhr.responseText); - if (resp.length !== 0){ - var results = []; - var stop = resp.length; - for (i = 0; i < resp.length; i++) { - if(resp[i] === undefined){ - stop = stop - 1; - continue; - } - var result = new Object(); - - if (that.getTrack(resp[i].title, "")){ - var track = resp[i].title; - if (track.indexOf(" - ") !== -1 && track.slice(track.indexOf(" - ") + 3).trim() !== ""){ - result.track = track.slice(track.indexOf(" - ") + 3).trim(); - result.artist = track.slice(0, track.indexOf(" - ")).trim(); - } - else if (track.indexOf(" -") !== -1 && track.slice(track.indexOf(" -") + 2).trim() !== ""){ - result.track = track.slice(track.indexOf(" -") + 2).trim(); - result.artist = track.slice(0, track.indexOf(" -")).trim(); - } - else if (track.indexOf(": ") !== -1 && track.slice(track.indexOf(": ") + 2).trim() !== ""){ - result.track = track.slice(track.indexOf(": ") + 2).trim(); - result.artist = track.slice(0, track.indexOf(": ")).trim(); - } - else if (track.indexOf("-") !== -1 && track.slice(track.indexOf("-") + 1).trim() !== ""){ - result.track = track.slice(track.indexOf("-") + 1).trim(); - result.artist = track.slice(0, track.indexOf("-")).trim(); - } - else if (track.indexOf(":") !== -1 && track.slice(track.indexOf(":") + 1).trim() !== ""){ - result.track = track.slice(track.indexOf(":") + 1).trim(); - result.artist = track.slice(0, track.indexOf(":")).trim(); - } - else if (track.indexOf("\u2014") !== -1 && track.slice(track.indexOf("\u2014") + 2).trim() !== ""){ - result.track = track.slice(track.indexOf("\u2014") + 2).trim(); - result.artist = track.slice(0, track.indexOf("\u2014")).trim(); - } - else if (resp[i].title !== "" && resp[i].user.username !== ""){ - // Last resort, the artist is the username - result.track = resp[i].title; - result.artist = resp[i].user.username; - } - else { - stop = stop - 1; - continue; - } - } - else { - stop = stop - 1; - continue; - } - - result.source = that.settings.name; - result.mimetype = "audio/mpeg"; - result.bitrate = 128; - result.duration = resp[i].duration / 1000; - result.score = 0.85; - result.year = resp[i].release_year; - result.url = resp[i].stream_url + ".json?client_id=TiNg2DRYhBnp01DA3zNag"; - if (resp[i].permalink_url !== undefined) result.linkUrl = resp[i].permalink_url; - - (function (i, result) { - var artist = encodeURIComponent(result.artist.capitalize()); - var url = "http://developer.echonest.com/api/v4/artist/extract?api_key=JRIHWEP6GPOER2QQ6&format=json&results=1&sort=hotttnesss-desc&text=" + artist; - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4){ - if (xhr.status === 200) { - var response = JSON.parse(xhr.responseText).response; - if (response && response.artists && response.artists.length > 0) { - artist = response.artists[0].name; - result.artist = artist; - result.id = i; - results.push(result); - stop = stop - 1; - } - else { - stop = stop - 1; - } - if (stop === 0) { - function sortResults(a, b){ - return a.id - b.id; - } - results = results.sort(sortResults); - for (var j = 0; j < results.length; j++){ - delete results[j].id; - } - var toReturn = { - results: results, - qid: qid - }; - Tomahawk.addTrackResults(toReturn); - } - } - else { - Tomahawk.log("Failed to do GET request to: " + url); - Tomahawk.log("Error: " + xhr.status + " " + xhr.statusText); - } - } - }; - xhr.send(null); - })(i, result); - } - if (stop === 0){ - Tomahawk.addTrackResults(empty); - } - } - else { - Tomahawk.addTrackResults(empty); - } - }); - } -}); - -Tomahawk.resolver.instance = SoundcloudResolver; diff --git a/assets/js/soundcloud/soundcloud.png b/assets/js/soundcloud/soundcloud.png deleted file mode 100644 index 3155f6025..000000000 Binary files a/assets/js/soundcloud/soundcloud.png and /dev/null differ diff --git a/assets/js/tomahawk.js b/assets/js/tomahawk.js deleted file mode 100644 index 1ca69e3f9..000000000 --- a/assets/js/tomahawk.js +++ /dev/null @@ -1,345 +0,0 @@ - -// if run in phantomjs add fake Tomahawk environment -if(window.Tomahawk === undefined) -{ -// alert("PHANTOMJS ENVIRONMENT"); - var Tomahawk = { - fakeEnv: function() - { - return true; - }, - resolverData: function() - { - return { - scriptPath: function() - { - return "/home/tomahawk/resolver.js"; - } - }; - }, - log: function( message ) - { - console.log( message ); - } - }; -} - - -Tomahawk.resolver = { - scriptPath: Tomahawk.resolverData().scriptPath -}; - -Tomahawk.timestamp = function() { - return Math.round( new Date()/1000 ); -}; - -Tomahawk.dumpResult = function( result ) { - var results = result.results; - Tomahawk.log("Dumping " + results.length + " results for query " + result.qid + "..."); - for(var i=0; i> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } - - function S (X, n) { return ( X >>> n ) | (X << (32 - n)); } - function R (X, n) { return ( X >>> n ); } - function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); } - function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); } - function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); } - function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); } - function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); } - function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); } - - function core_sha256 (m, l) { - var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2); - var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19); - var W = new Array(64); - var a, b, c, d, e, f, g, h, i, j; - var T1, T2; - - m[l >> 5] |= 0x80 << (24 - l % 32); - m[((l + 64 >> 9) << 4) + 15] = l; - - for ( i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); - } - return bin; - } - - function Utf8Encode(string) { - string = string.replace(/\r\n/g,"\n"); - var utftext = ""; - - for (var n = 0; n < string.length; n++) { - - var c = string.charCodeAt(n); - - if (c < 128) { - utftext += String.fromCharCode(c); - } - else if((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } - else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } - - } - - return utftext; - } - - function binb2hex (binarray) { - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var str = ""; - for(var i = 0; i < binarray.length * 4; i++) { - str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + - hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); - } - return str; - } - - s = Utf8Encode(s); - return binb2hex(core_sha256(str2binb(s), s.length * chrsz)); - -}; - - - -// some aliases -Tomahawk.setTimeout = window.setTimeout; -Tomahawk.setInterval = window.setInterval; diff --git a/assets/js/tomahawk_android.js b/assets/js/tomahawk_android.js deleted file mode 100644 index 2d6698129..000000000 --- a/assets/js/tomahawk_android.js +++ /dev/null @@ -1,9 +0,0 @@ -Tomahawk.resolverData = - function () { - return JSON.parse(Tomahawk.resolverDataString()); - }; - -Tomahawk.addTrackResults = - function (results) { - Tomahawk.addTrackResultsString(JSON.stringify(results)); - } \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..3bae6759a --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +buildscript { + apply from: "$rootProject.projectDir/gradle/script/git-version.gradle" + + repositories { + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:2.1.2" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenCentral() + jcenter() + } +} diff --git a/gradle/script/git-version.gradle b/gradle/script/git-version.gradle new file mode 100644 index 000000000..0cf545f51 --- /dev/null +++ b/gradle/script/git-version.gradle @@ -0,0 +1,75 @@ +/** + * Copyright 2016 Avinash Ananth Narayan R + * 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. + */ +buildscript { + dependencies { + //noinspection GradleDynamicVersion + classpath "org.eclipse.jgit:org.eclipse.jgit:4.1.1.+" + } + repositories { + jcenter() + } +} +/* source: https://gist.githubusercontent.com/Avinash-Bhat/12fc59b3d96cb427e5e9*/ +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.storage.file.FileRepositoryBuilder + +import static org.eclipse.jgit.lib.Constants.MASTER + +def gitFile = projectDir; +if (gitFile == null) { + gitFile = new File("").getAbsoluteFile() +} +def gitRepo = new FileRepositoryBuilder() + .readEnvironment() + .findGitDir(gitFile) + .build() +def git = Git.wrap(gitRepo) + +ext.readVersionCode = { + def walk = new RevWalk(gitRepo) + def masterRef = gitRepo.getRef(MASTER) + if (masterRef == null) { + return 1; + } + def head = walk.parseCommit(masterRef.getObjectId()) + try { + def count = 0 + while (head != null) { + count++ + def parents = head.getParents() + if (parents != null && parents.length > 0) { + head = walk.parseCommit(parents[0]) + } else { + head = null + } + } + walk.dispose() + return count + } finally { + walk.close() + } +} + +ext.readVersionName = { + try { + def tag = git.describe().setLong(false).call() + def clean = git.status().call().isClean() + def version = tag + (clean ? '' : '_modified') + return version + } catch (ignore) { + return "1.0_nogit" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..8c0fb64a8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..dbdc05d27 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ic_launcher-web.png b/ic_launcher-web.png deleted file mode 100644 index e6c0df5d5..000000000 Binary files a/ic_launcher-web.png and /dev/null differ diff --git a/libs/acra-4.2.3.jar b/libs/acra-4.2.3.jar deleted file mode 100644 index c6b57a9d7..000000000 Binary files a/libs/acra-4.2.3.jar and /dev/null differ diff --git a/libs/stickylistheaders-1.0.0-SNAPSHOT.jar b/libs/stickylistheaders-1.0.0-SNAPSHOT.jar deleted file mode 100644 index 6ae3a6f9c..000000000 Binary files a/libs/stickylistheaders-1.0.0-SNAPSHOT.jar and /dev/null differ diff --git a/proguard-android.txt b/proguard-android.txt new file mode 100644 index 000000000..2b44932fc --- /dev/null +++ b/proguard-android.txt @@ -0,0 +1,213 @@ +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*,!code/allocation/variable +-optimizationpasses 5 +-allowaccessmodification +-dontpreverify +-dontobfuscate +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-verbose + +-keepattributes Signature,*Annotation*,EnclosingMethod +-keep public class com.google.vending.licensing.ILicensingService +-keep public class com.android.vending.licensing.ILicensingService + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-keep class * implements java.io.Serializable { + *; +} +-keepclassmembers class **.R$* { + public static ; +} + +-keep class se.emilsjolander.stickylistheaders.** { + *; +} + +############################### +#Spotify Android SDK specifics# +############################### +-keep class com.spotify.** { *; } + +###################### +#LibVLC specifics# +###################### +-keep class org.videolan.libvlc.** { *; } + +################################ +#Javascript Interface specifics# +################################ +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} + +########################### +#Support library specifics# +########################### +-keep class android.support.v7.** { + *; +} +-keep interface android.support.v7.** { + *; +} +-keep class android.support.v8.renderscript.** { + *; +} +-dontwarn android.support.v4.** +-dontwarn android.support.v7.** +-dontwarn android.support.v8.** + +################### +#Jackson specifics# +################### +-keepnames class org.codehaus.jackson.** { + *; +} +-keepclassmembers public class * { + public (...); +} +-keep class org.tomahawk.libtomahawk.infosystem.deserializer.** { *; } +-dontwarn org.joda.time.** +-dontwarn org.w3c.dom.bootstrap.** + +################# +#Guava specifics# +################# +-dontwarn sun.misc.Unsafe +-dontwarn com.google.common.collect.MinMaxPriorityQueue +-keepclasseswithmembers public class * { + public static void main(java.lang.String[]); +} + +################### +#Picasso specifics# +################### +-dontwarn com.squareup.okhttp.** + +#################### +#Retrofit specifics# +#################### +-keep class org.tomahawk.libtomahawk.infosystem.hatchet.Hatchet { *; } +-keep class org.tomahawk.libtomahawk.authentication.HatchetAuth { *; } +-keep class com.google.gson.** { *; } +-keep class com.google.inject.* { *; } +-keep class org.apache.http.** { *; } +-keep class org.apache.james.mime4j.** { *; } +-keep class javax.inject.** { *; } +-keep class retrofit.** { *; } +-keep class rx.** { *; } +-keep class sun.misc.Unsafe { *; } +-keepclasseswithmembers class * { + @retrofit.http.* ; +} +-dontwarn rx.* +-dontwarn retrofit.** + +################## +#OkHttp specifics# +################## +-dontwarn okio.** + +############################### +#greenrobot EventBus specifics# +############################### +-keepclassmembers class ** { + public void onEvent*(**); +} + +################## +#Lucene specifics# +################## +-dontwarn org.apache.regexp.** +-dontwarn org.apache.commons.codec.binary.** +-dontwarn java.lang.management.** +-keep class org.apache.regexp.** +-keep class org.apache.commons.codec.binary.** +-keep class java.lang.management.** +-keep class org.apache.lucene.codecs.Codec +-keep class * extends org.apache.lucene.codecs.Codec +-keep class org.apache.lucene.codecs.PostingsFormat +-keep class * extends org.apache.lucene.codecs.PostingsFormat +-keep class org.apache.lucene.codecs.DocValuesFormat +-keep class * extends org.apache.lucene.codecs.DocValuesFormat +-keep class org.apache.lucene.analysis.tokenattributes.** +-keep class org.apache.lucene.**Attribute +-keep class * implements org.apache.lucene.**Attribute + +################################### +#UnifiedMetaDataProvider specifics# +################################### +-dontwarn android.media.IRemoteControlDisplay** +-dontwarn android.media.AudioManager +-keep class org.electricwisdom.** { *; } +-keep interface org.electricwisdom.** { *; } + +#################### +#Signpost specifics# +#################### +-dontwarn oauth.signpost.** + +################ +#ACRA specifics# +################ +-dontwarn org.apache.http.** +-dontwarn org.acra.ErrorReporter +# Restore some Source file names and restore approximate line numbers in the stack traces, +# otherwise the stack traces are pretty useless +-keepattributes SourceFile,LineNumberTable +# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'. +# Note: if you are removing log messages elsewhere in this file then this isn't necessary +-keep class org.acra.ACRA { + *; +} +# keep this around for some enums that ACRA needs +-keep class org.acra.ReportingInteractionMode { + *; +} +-keepnames class org.acra.sender.HttpSender$** { + *; +} +-keepnames class org.acra.ReportField { + *; +} +# keep this otherwise it is removed by ProGuard +-keep public class org.acra.ErrorReporter { + public void addCustomData(java.lang.String,java.lang.String); + public void putCustomData(java.lang.String,java.lang.String); + public void removeCustomData(java.lang.String); +} +# keep this otherwise it is removed by ProGuard +-keep public class org.acra.ErrorReporter { + public void handleSilentException(java.lang.Throwable); +} + +################## +#Deezer specifics# +################## +-keep class com.deezer.** { *; } + +##################### +#UserVoice specifics# +##################### +-dontwarn android.net.http.** +-dontwarn com.google.android.gms.** diff --git a/res/anim/slide_in_left.xml b/res/anim/slide_in_left.xml deleted file mode 100644 index 74fd5a621..000000000 --- a/res/anim/slide_in_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/anim/slide_out_right.xml b/res/anim/slide_out_right.xml deleted file mode 100644 index 57b2f5026..000000000 --- a/res/anim/slide_out_right.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/drawable-hdpi/ab_solid_tomahawk.9.png b/res/drawable-hdpi/ab_solid_tomahawk.9.png deleted file mode 100644 index d431708b7..000000000 Binary files a/res/drawable-hdpi/ab_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/ab_split_transparent_tomahawk.9.png b/res/drawable-hdpi/ab_split_transparent_tomahawk.9.png deleted file mode 100644 index d7338de08..000000000 Binary files a/res/drawable-hdpi/ab_split_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png b/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png deleted file mode 100644 index 005445acc..000000000 Binary files a/res/drawable-hdpi/ab_stacked_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/ab_transparent_tomahawk.9.png b/res/drawable-hdpi/ab_transparent_tomahawk.9.png deleted file mode 100644 index cc1a81976..000000000 Binary files a/res/drawable-hdpi/ab_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png b/res/drawable-hdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png deleted file mode 100644 index 2abc45809..000000000 Binary files a/res/drawable-hdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_album.png b/res/drawable-hdpi/ic_action_album.png deleted file mode 100644 index 4c9ae61a1..000000000 Binary files a/res/drawable-hdpi/ic_action_album.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_artist.png b/res/drawable-hdpi/ic_action_artist.png deleted file mode 100644 index 27c31b49f..000000000 Binary files a/res/drawable-hdpi/ic_action_artist.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_back.png b/res/drawable-hdpi/ic_action_back.png deleted file mode 100644 index 657054d44..000000000 Binary files a/res/drawable-hdpi/ic_action_back.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_collection.png b/res/drawable-hdpi/ic_action_collection.png deleted file mode 100755 index 1ecd93016..000000000 Binary files a/res/drawable-hdpi/ic_action_collection.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_down.png b/res/drawable-hdpi/ic_action_down.png deleted file mode 100644 index d9a80471e..000000000 Binary files a/res/drawable-hdpi/ic_action_down.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_next.png b/res/drawable-hdpi/ic_action_next.png deleted file mode 100755 index 1d6666192..000000000 Binary files a/res/drawable-hdpi/ic_action_next.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_playlist.png b/res/drawable-hdpi/ic_action_playlist.png deleted file mode 100644 index ae040e4bb..000000000 Binary files a/res/drawable-hdpi/ic_action_playlist.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_refresh.png b/res/drawable-hdpi/ic_action_refresh.png deleted file mode 100644 index 08c32e09e..000000000 Binary files a/res/drawable-hdpi/ic_action_refresh.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_search.png b/res/drawable-hdpi/ic_action_search.png deleted file mode 100644 index 67de12dec..000000000 Binary files a/res/drawable-hdpi/ic_action_search.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_settings.png b/res/drawable-hdpi/ic_action_settings.png deleted file mode 100644 index d57b29053..000000000 Binary files a/res/drawable-hdpi/ic_action_settings.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_track.png b/res/drawable-hdpi/ic_action_track.png deleted file mode 100644 index d38a37176..000000000 Binary files a/res/drawable-hdpi/ic_action_track.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_action_up.png b/res/drawable-hdpi/ic_action_up.png deleted file mode 100644 index 58ba24226..000000000 Binary files a/res/drawable-hdpi/ic_action_up.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 512a14345..000000000 Binary files a/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_exit.png b/res/drawable-hdpi/ic_player_exit.png deleted file mode 100644 index 956261a31..000000000 Binary files a/res/drawable-hdpi/ic_player_exit.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_next.png b/res/drawable-hdpi/ic_player_next.png deleted file mode 100644 index ca4cc0e0c..000000000 Binary files a/res/drawable-hdpi/ic_player_next.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_pause.png b/res/drawable-hdpi/ic_player_pause.png deleted file mode 100644 index a183a5476..000000000 Binary files a/res/drawable-hdpi/ic_player_pause.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_play.png b/res/drawable-hdpi/ic_player_play.png deleted file mode 100644 index 1e1820d8c..000000000 Binary files a/res/drawable-hdpi/ic_player_play.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_previous.png b/res/drawable-hdpi/ic_player_previous.png deleted file mode 100644 index fc9a50215..000000000 Binary files a/res/drawable-hdpi/ic_player_previous.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_repeat.png b/res/drawable-hdpi/ic_player_repeat.png deleted file mode 100644 index 2326ce0cd..000000000 Binary files a/res/drawable-hdpi/ic_player_repeat.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_player_shuffle.png b/res/drawable-hdpi/ic_player_shuffle.png deleted file mode 100644 index b827533ab..000000000 Binary files a/res/drawable-hdpi/ic_player_shuffle.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_playlist_is_playing.png b/res/drawable-hdpi/ic_playlist_is_playing.png deleted file mode 100644 index fcf8397e2..000000000 Binary files a/res/drawable-hdpi/ic_playlist_is_playing.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_resolver_default.png b/res/drawable-hdpi/ic_resolver_default.png deleted file mode 100644 index dbd710ecf..000000000 Binary files a/res/drawable-hdpi/ic_resolver_default.png and /dev/null differ diff --git a/res/drawable-hdpi/ic_settings_account.png b/res/drawable-hdpi/ic_settings_account.png deleted file mode 100644 index 030978c9b..000000000 Binary files a/res/drawable-hdpi/ic_settings_account.png and /dev/null differ diff --git a/res/drawable-hdpi/list_activated_holo.9.png b/res/drawable-hdpi/list_activated_holo.9.png deleted file mode 100644 index 328bfd392..000000000 Binary files a/res/drawable-hdpi/list_activated_holo.9.png and /dev/null differ diff --git a/res/drawable-hdpi/list_focused_holo.9.png b/res/drawable-hdpi/list_focused_holo.9.png deleted file mode 100644 index 2c472491a..000000000 Binary files a/res/drawable-hdpi/list_focused_holo.9.png and /dev/null differ diff --git a/res/drawable-hdpi/list_longpressed_holo.9.png b/res/drawable-hdpi/list_longpressed_holo.9.png deleted file mode 100644 index 328bfd392..000000000 Binary files a/res/drawable-hdpi/list_longpressed_holo.9.png and /dev/null differ diff --git a/res/drawable-hdpi/list_pressed_holo_dark.9.png b/res/drawable-hdpi/list_pressed_holo_dark.9.png deleted file mode 100644 index c085372df..000000000 Binary files a/res/drawable-hdpi/list_pressed_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png b/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png deleted file mode 100644 index f6fd30dcd..000000000 Binary files a/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/menu_dropdown_panel_tomahawk.9.png b/res/drawable-hdpi/menu_dropdown_panel_tomahawk.9.png deleted file mode 100755 index 89ddc1c51..000000000 Binary files a/res/drawable-hdpi/menu_dropdown_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/menu_hardkey_panel_tomahawk.9.png b/res/drawable-hdpi/menu_hardkey_panel_tomahawk.9.png deleted file mode 100755 index 084feadcc..000000000 Binary files a/res/drawable-hdpi/menu_hardkey_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/no_album_art_placeholder.png b/res/drawable-hdpi/no_album_art_placeholder.png deleted file mode 100644 index 49f6b0482..000000000 Binary files a/res/drawable-hdpi/no_album_art_placeholder.png and /dev/null differ diff --git a/res/drawable-hdpi/no_artist_placeholder.png b/res/drawable-hdpi/no_artist_placeholder.png deleted file mode 100644 index 81fcfee08..000000000 Binary files a/res/drawable-hdpi/no_artist_placeholder.png and /dev/null differ diff --git a/res/drawable-hdpi/progress_bg_tomahawk.9.png b/res/drawable-hdpi/progress_bg_tomahawk.9.png deleted file mode 100644 index 3d5c707d5..000000000 Binary files a/res/drawable-hdpi/progress_bg_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/progress_indeterminate_drawable_tomahawk.png b/res/drawable-hdpi/progress_indeterminate_drawable_tomahawk.png deleted file mode 100644 index 1ddf99631..000000000 Binary files a/res/drawable-hdpi/progress_indeterminate_drawable_tomahawk.png and /dev/null differ diff --git a/res/drawable-hdpi/progress_primary_tomahawk.9.png b/res/drawable-hdpi/progress_primary_tomahawk.9.png deleted file mode 100644 index 993d837a3..000000000 Binary files a/res/drawable-hdpi/progress_primary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/progress_secondary_tomahawk.9.png b/res/drawable-hdpi/progress_secondary_tomahawk.9.png deleted file mode 100644 index 4dad860b2..000000000 Binary files a/res/drawable-hdpi/progress_secondary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/progress_thumb_tomahawk.png b/res/drawable-hdpi/progress_thumb_tomahawk.png deleted file mode 100644 index 860c13a8c..000000000 Binary files a/res/drawable-hdpi/progress_thumb_tomahawk.png and /dev/null differ diff --git a/res/drawable-hdpi/spinner_ab_default_tomahawk.9.png b/res/drawable-hdpi/spinner_ab_default_tomahawk.9.png deleted file mode 100644 index 4fd4aeba0..000000000 Binary files a/res/drawable-hdpi/spinner_ab_default_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/spinner_ab_disabled_tomahawk.9.png b/res/drawable-hdpi/spinner_ab_disabled_tomahawk.9.png deleted file mode 100644 index d42c97b85..000000000 Binary files a/res/drawable-hdpi/spinner_ab_disabled_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/spinner_ab_focused_tomahawk.9.png b/res/drawable-hdpi/spinner_ab_focused_tomahawk.9.png deleted file mode 100644 index a4563a221..000000000 Binary files a/res/drawable-hdpi/spinner_ab_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/spinner_ab_pressed_tomahawk.9.png b/res/drawable-hdpi/spinner_ab_pressed_tomahawk.9.png deleted file mode 100644 index b2442c856..000000000 Binary files a/res/drawable-hdpi/spinner_ab_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/tab_selected_focused_tomahawk.9.png b/res/drawable-hdpi/tab_selected_focused_tomahawk.9.png deleted file mode 100644 index 3d147c418..000000000 Binary files a/res/drawable-hdpi/tab_selected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/tab_selected_pressed_tomahawk.9.png b/res/drawable-hdpi/tab_selected_pressed_tomahawk.9.png deleted file mode 100644 index b0adafb7f..000000000 Binary files a/res/drawable-hdpi/tab_selected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/tab_selected_tomahawk.9.png b/res/drawable-hdpi/tab_selected_tomahawk.9.png deleted file mode 100644 index 96fc3b70e..000000000 Binary files a/res/drawable-hdpi/tab_selected_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/tab_unselected_focused_tomahawk.9.png b/res/drawable-hdpi/tab_unselected_focused_tomahawk.9.png deleted file mode 100644 index 877acefac..000000000 Binary files a/res/drawable-hdpi/tab_unselected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/tab_unselected_pressed_tomahawk.9.png b/res/drawable-hdpi/tab_unselected_pressed_tomahawk.9.png deleted file mode 100644 index 003616042..000000000 Binary files a/res/drawable-hdpi/tab_unselected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-hdpi/textfield_activated_holo_dark.9.png b/res/drawable-hdpi/textfield_activated_holo_dark.9.png deleted file mode 100644 index 7ef5c1de6..000000000 Binary files a/res/drawable-hdpi/textfield_activated_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/textfield_default_holo_dark.9.png b/res/drawable-hdpi/textfield_default_holo_dark.9.png deleted file mode 100644 index 23d3a5961..000000000 Binary files a/res/drawable-hdpi/textfield_default_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/textfield_disabled_focused_holo_dark.9.png b/res/drawable-hdpi/textfield_disabled_focused_holo_dark.9.png deleted file mode 100644 index fe753154c..000000000 Binary files a/res/drawable-hdpi/textfield_disabled_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/textfield_disabled_holo_dark.9.png b/res/drawable-hdpi/textfield_disabled_holo_dark.9.png deleted file mode 100644 index 769bc0a1b..000000000 Binary files a/res/drawable-hdpi/textfield_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-hdpi/textfield_focused_holo_dark.9.png b/res/drawable-hdpi/textfield_focused_holo_dark.9.png deleted file mode 100644 index 651529d83..000000000 Binary files a/res/drawable-hdpi/textfield_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/ab_solid_tomahawk.9.png b/res/drawable-mdpi/ab_solid_tomahawk.9.png deleted file mode 100644 index a5826a233..000000000 Binary files a/res/drawable-mdpi/ab_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/ab_split_transparent_tomahawk.9.png b/res/drawable-mdpi/ab_split_transparent_tomahawk.9.png deleted file mode 100644 index 48c04ca6a..000000000 Binary files a/res/drawable-mdpi/ab_split_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png b/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png deleted file mode 100644 index fd09cb186..000000000 Binary files a/res/drawable-mdpi/ab_stacked_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/ab_transparent_tomahawk.9.png b/res/drawable-mdpi/ab_transparent_tomahawk.9.png deleted file mode 100644 index 6766bb5b7..000000000 Binary files a/res/drawable-mdpi/ab_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/abs__ab_share_pack_holo_dark.9.png b/res/drawable-mdpi/abs__ab_share_pack_holo_dark.9.png deleted file mode 100644 index ed4ba34ec..000000000 Binary files a/res/drawable-mdpi/abs__ab_share_pack_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_album.png b/res/drawable-mdpi/ic_action_album.png deleted file mode 100644 index 37d3897d9..000000000 Binary files a/res/drawable-mdpi/ic_action_album.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_artist.png b/res/drawable-mdpi/ic_action_artist.png deleted file mode 100644 index 2398215eb..000000000 Binary files a/res/drawable-mdpi/ic_action_artist.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_back.png b/res/drawable-mdpi/ic_action_back.png deleted file mode 100644 index dc675d5b5..000000000 Binary files a/res/drawable-mdpi/ic_action_back.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_collection.png b/res/drawable-mdpi/ic_action_collection.png deleted file mode 100755 index 9f1559a4f..000000000 Binary files a/res/drawable-mdpi/ic_action_collection.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_down.png b/res/drawable-mdpi/ic_action_down.png deleted file mode 100644 index 52627313d..000000000 Binary files a/res/drawable-mdpi/ic_action_down.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_next.png b/res/drawable-mdpi/ic_action_next.png deleted file mode 100755 index 1eea4b0d6..000000000 Binary files a/res/drawable-mdpi/ic_action_next.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_playlist.png b/res/drawable-mdpi/ic_action_playlist.png deleted file mode 100644 index 9770d80d7..000000000 Binary files a/res/drawable-mdpi/ic_action_playlist.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_refresh.png b/res/drawable-mdpi/ic_action_refresh.png deleted file mode 100644 index 55c43c327..000000000 Binary files a/res/drawable-mdpi/ic_action_refresh.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_search.png b/res/drawable-mdpi/ic_action_search.png deleted file mode 100644 index 134d5490b..000000000 Binary files a/res/drawable-mdpi/ic_action_search.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_settings.png b/res/drawable-mdpi/ic_action_settings.png deleted file mode 100644 index d90f1255e..000000000 Binary files a/res/drawable-mdpi/ic_action_settings.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_track.png b/res/drawable-mdpi/ic_action_track.png deleted file mode 100644 index e7adb7f0f..000000000 Binary files a/res/drawable-mdpi/ic_action_track.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_action_up.png b/res/drawable-mdpi/ic_action_up.png deleted file mode 100644 index a49cf0764..000000000 Binary files a/res/drawable-mdpi/ic_action_up.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 6a88abe1f..000000000 Binary files a/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_exit.png b/res/drawable-mdpi/ic_player_exit.png deleted file mode 100644 index 2cb13649e..000000000 Binary files a/res/drawable-mdpi/ic_player_exit.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_next.png b/res/drawable-mdpi/ic_player_next.png deleted file mode 100644 index 36ae12593..000000000 Binary files a/res/drawable-mdpi/ic_player_next.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_pause.png b/res/drawable-mdpi/ic_player_pause.png deleted file mode 100644 index fe1561e13..000000000 Binary files a/res/drawable-mdpi/ic_player_pause.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_play.png b/res/drawable-mdpi/ic_player_play.png deleted file mode 100644 index a8eff260c..000000000 Binary files a/res/drawable-mdpi/ic_player_play.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_previous.png b/res/drawable-mdpi/ic_player_previous.png deleted file mode 100644 index 0d133804c..000000000 Binary files a/res/drawable-mdpi/ic_player_previous.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_repeat.png b/res/drawable-mdpi/ic_player_repeat.png deleted file mode 100644 index 834ecbaff..000000000 Binary files a/res/drawable-mdpi/ic_player_repeat.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_player_shuffle.png b/res/drawable-mdpi/ic_player_shuffle.png deleted file mode 100644 index fdbb0fdb0..000000000 Binary files a/res/drawable-mdpi/ic_player_shuffle.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_playlist_is_playing.png b/res/drawable-mdpi/ic_playlist_is_playing.png deleted file mode 100644 index e80433848..000000000 Binary files a/res/drawable-mdpi/ic_playlist_is_playing.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_resolver_default.png b/res/drawable-mdpi/ic_resolver_default.png deleted file mode 100644 index 31053364d..000000000 Binary files a/res/drawable-mdpi/ic_resolver_default.png and /dev/null differ diff --git a/res/drawable-mdpi/ic_settings_account.png b/res/drawable-mdpi/ic_settings_account.png deleted file mode 100644 index c350a2f4c..000000000 Binary files a/res/drawable-mdpi/ic_settings_account.png and /dev/null differ diff --git a/res/drawable-mdpi/list_activated_holo.9.png b/res/drawable-mdpi/list_activated_holo.9.png deleted file mode 100644 index 146460d78..000000000 Binary files a/res/drawable-mdpi/list_activated_holo.9.png and /dev/null differ diff --git a/res/drawable-mdpi/list_focused_holo.9.png b/res/drawable-mdpi/list_focused_holo.9.png deleted file mode 100644 index 5769947df..000000000 Binary files a/res/drawable-mdpi/list_focused_holo.9.png and /dev/null differ diff --git a/res/drawable-mdpi/list_longpressed_holo.9.png b/res/drawable-mdpi/list_longpressed_holo.9.png deleted file mode 100644 index 146460d78..000000000 Binary files a/res/drawable-mdpi/list_longpressed_holo.9.png and /dev/null differ diff --git a/res/drawable-mdpi/list_pressed_holo_dark.9.png b/res/drawable-mdpi/list_pressed_holo_dark.9.png deleted file mode 100644 index 0ecd69aa3..000000000 Binary files a/res/drawable-mdpi/list_pressed_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png b/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png deleted file mode 100644 index 92da2f0dd..000000000 Binary files a/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/menu_dropdown_panel_tomahawk.9.png b/res/drawable-mdpi/menu_dropdown_panel_tomahawk.9.png deleted file mode 100755 index 08d0e7b56..000000000 Binary files a/res/drawable-mdpi/menu_dropdown_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/menu_hardkey_panel_tomahawk.9.png b/res/drawable-mdpi/menu_hardkey_panel_tomahawk.9.png deleted file mode 100755 index 288fc9c09..000000000 Binary files a/res/drawable-mdpi/menu_hardkey_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/no_album_art_placeholder.png b/res/drawable-mdpi/no_album_art_placeholder.png deleted file mode 100644 index b6ea52317..000000000 Binary files a/res/drawable-mdpi/no_album_art_placeholder.png and /dev/null differ diff --git a/res/drawable-mdpi/progress_bg_tomahawk.9.png b/res/drawable-mdpi/progress_bg_tomahawk.9.png deleted file mode 100644 index 9372a60f7..000000000 Binary files a/res/drawable-mdpi/progress_bg_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/progress_indeterminate_drawable_tomahawk.png b/res/drawable-mdpi/progress_indeterminate_drawable_tomahawk.png deleted file mode 100644 index a74a1864f..000000000 Binary files a/res/drawable-mdpi/progress_indeterminate_drawable_tomahawk.png and /dev/null differ diff --git a/res/drawable-mdpi/progress_primary_tomahawk.9.png b/res/drawable-mdpi/progress_primary_tomahawk.9.png deleted file mode 100644 index 4e7bf6994..000000000 Binary files a/res/drawable-mdpi/progress_primary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/progress_secondary_tomahawk.9.png b/res/drawable-mdpi/progress_secondary_tomahawk.9.png deleted file mode 100644 index 01c09fa9a..000000000 Binary files a/res/drawable-mdpi/progress_secondary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/progress_thumb_tomahawk.png b/res/drawable-mdpi/progress_thumb_tomahawk.png deleted file mode 100644 index a073d22bc..000000000 Binary files a/res/drawable-mdpi/progress_thumb_tomahawk.png and /dev/null differ diff --git a/res/drawable-mdpi/spinner_ab_default_tomahawk.9.png b/res/drawable-mdpi/spinner_ab_default_tomahawk.9.png deleted file mode 100644 index 9aeafee20..000000000 Binary files a/res/drawable-mdpi/spinner_ab_default_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/spinner_ab_disabled_tomahawk.9.png b/res/drawable-mdpi/spinner_ab_disabled_tomahawk.9.png deleted file mode 100644 index 88dd44151..000000000 Binary files a/res/drawable-mdpi/spinner_ab_disabled_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/spinner_ab_focused_tomahawk.9.png b/res/drawable-mdpi/spinner_ab_focused_tomahawk.9.png deleted file mode 100644 index 4862d12c7..000000000 Binary files a/res/drawable-mdpi/spinner_ab_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/spinner_ab_pressed_tomahawk.9.png b/res/drawable-mdpi/spinner_ab_pressed_tomahawk.9.png deleted file mode 100644 index 6bf313576..000000000 Binary files a/res/drawable-mdpi/spinner_ab_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/tab_selected_focused_tomahawk.9.png b/res/drawable-mdpi/tab_selected_focused_tomahawk.9.png deleted file mode 100644 index 8c5147bc7..000000000 Binary files a/res/drawable-mdpi/tab_selected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/tab_selected_pressed_tomahawk.9.png b/res/drawable-mdpi/tab_selected_pressed_tomahawk.9.png deleted file mode 100644 index b85bdb417..000000000 Binary files a/res/drawable-mdpi/tab_selected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/tab_selected_tomahawk.9.png b/res/drawable-mdpi/tab_selected_tomahawk.9.png deleted file mode 100644 index e760ff426..000000000 Binary files a/res/drawable-mdpi/tab_selected_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/tab_unselected_focused_tomahawk.9.png b/res/drawable-mdpi/tab_unselected_focused_tomahawk.9.png deleted file mode 100644 index d003262e9..000000000 Binary files a/res/drawable-mdpi/tab_unselected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/tab_unselected_pressed_tomahawk.9.png b/res/drawable-mdpi/tab_unselected_pressed_tomahawk.9.png deleted file mode 100644 index d9c765c2d..000000000 Binary files a/res/drawable-mdpi/tab_unselected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-mdpi/textfield_activated_holo_dark.9.png b/res/drawable-mdpi/textfield_activated_holo_dark.9.png deleted file mode 100644 index 48e92dd9d..000000000 Binary files a/res/drawable-mdpi/textfield_activated_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/textfield_default_holo_dark.9.png b/res/drawable-mdpi/textfield_default_holo_dark.9.png deleted file mode 100644 index 23a462f0e..000000000 Binary files a/res/drawable-mdpi/textfield_default_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/textfield_disabled_focused_holo_dark.9.png b/res/drawable-mdpi/textfield_disabled_focused_holo_dark.9.png deleted file mode 100644 index e922f714d..000000000 Binary files a/res/drawable-mdpi/textfield_disabled_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/textfield_disabled_holo_dark.9.png b/res/drawable-mdpi/textfield_disabled_holo_dark.9.png deleted file mode 100644 index 7e44919a7..000000000 Binary files a/res/drawable-mdpi/textfield_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-mdpi/textfield_focused_holo_dark.9.png b/res/drawable-mdpi/textfield_focused_holo_dark.9.png deleted file mode 100644 index a240251ef..000000000 Binary files a/res/drawable-mdpi/textfield_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png b/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png deleted file mode 100644 index 731982551..000000000 Binary files a/res/drawable-xhdpi/ab_bottom_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/ab_solid_tomahawk.9.png b/res/drawable-xhdpi/ab_solid_tomahawk.9.png deleted file mode 100644 index ef68ff63d..000000000 Binary files a/res/drawable-xhdpi/ab_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/ab_split_transparent_tomahawk.9.png b/res/drawable-xhdpi/ab_split_transparent_tomahawk.9.png deleted file mode 100644 index a69b5220e..000000000 Binary files a/res/drawable-xhdpi/ab_split_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png b/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png deleted file mode 100644 index f9a1e1f2a..000000000 Binary files a/res/drawable-xhdpi/ab_stacked_solid_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/ab_transparent_tomahawk.9.png b/res/drawable-xhdpi/ab_transparent_tomahawk.9.png deleted file mode 100644 index a408e04a7..000000000 Binary files a/res/drawable-xhdpi/ab_transparent_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png b/res/drawable-xhdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png deleted file mode 100644 index a92fb1d4a..000000000 Binary files a/res/drawable-xhdpi/abs__ic_menu_moreoverflow_normal_holo_dark.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_album.png b/res/drawable-xhdpi/ic_action_album.png deleted file mode 100644 index 64659b7be..000000000 Binary files a/res/drawable-xhdpi/ic_action_album.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_artist.png b/res/drawable-xhdpi/ic_action_artist.png deleted file mode 100644 index f989ffb31..000000000 Binary files a/res/drawable-xhdpi/ic_action_artist.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_back.png b/res/drawable-xhdpi/ic_action_back.png deleted file mode 100644 index b03529e16..000000000 Binary files a/res/drawable-xhdpi/ic_action_back.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_collection.png b/res/drawable-xhdpi/ic_action_collection.png deleted file mode 100755 index 920613ccf..000000000 Binary files a/res/drawable-xhdpi/ic_action_collection.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_down.png b/res/drawable-xhdpi/ic_action_down.png deleted file mode 100644 index 2368b67b6..000000000 Binary files a/res/drawable-xhdpi/ic_action_down.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_next.png b/res/drawable-xhdpi/ic_action_next.png deleted file mode 100755 index 49b456400..000000000 Binary files a/res/drawable-xhdpi/ic_action_next.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_playlist.png b/res/drawable-xhdpi/ic_action_playlist.png deleted file mode 100644 index 1a88f0798..000000000 Binary files a/res/drawable-xhdpi/ic_action_playlist.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_search.png b/res/drawable-xhdpi/ic_action_search.png deleted file mode 100644 index d699c6b37..000000000 Binary files a/res/drawable-xhdpi/ic_action_search.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_settings.png b/res/drawable-xhdpi/ic_action_settings.png deleted file mode 100644 index f2572c846..000000000 Binary files a/res/drawable-xhdpi/ic_action_settings.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_track.png b/res/drawable-xhdpi/ic_action_track.png deleted file mode 100644 index 8c983a21e..000000000 Binary files a/res/drawable-xhdpi/ic_action_track.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_action_up.png b/res/drawable-xhdpi/ic_action_up.png deleted file mode 100644 index 447301e95..000000000 Binary files a/res/drawable-xhdpi/ic_action_up.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index e00811471..000000000 Binary files a/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_menu_account.png b/res/drawable-xhdpi/ic_menu_account.png deleted file mode 100644 index 789d740c8..000000000 Binary files a/res/drawable-xhdpi/ic_menu_account.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_exit.png b/res/drawable-xhdpi/ic_player_exit.png deleted file mode 100644 index a0614c35a..000000000 Binary files a/res/drawable-xhdpi/ic_player_exit.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_next.png b/res/drawable-xhdpi/ic_player_next.png deleted file mode 100644 index 6c3b14765..000000000 Binary files a/res/drawable-xhdpi/ic_player_next.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_pause.png b/res/drawable-xhdpi/ic_player_pause.png deleted file mode 100644 index 11eee13c2..000000000 Binary files a/res/drawable-xhdpi/ic_player_pause.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_play.png b/res/drawable-xhdpi/ic_player_play.png deleted file mode 100644 index 1b785e263..000000000 Binary files a/res/drawable-xhdpi/ic_player_play.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_previous.png b/res/drawable-xhdpi/ic_player_previous.png deleted file mode 100644 index a46e3808b..000000000 Binary files a/res/drawable-xhdpi/ic_player_previous.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_repeat.png b/res/drawable-xhdpi/ic_player_repeat.png deleted file mode 100644 index 48c6c62fe..000000000 Binary files a/res/drawable-xhdpi/ic_player_repeat.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_player_shuffle.png b/res/drawable-xhdpi/ic_player_shuffle.png deleted file mode 100644 index c860f22bc..000000000 Binary files a/res/drawable-xhdpi/ic_player_shuffle.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_playlist_is_playing.png b/res/drawable-xhdpi/ic_playlist_is_playing.png deleted file mode 100644 index 6f9586bb4..000000000 Binary files a/res/drawable-xhdpi/ic_playlist_is_playing.png and /dev/null differ diff --git a/res/drawable-xhdpi/ic_resolver_default.png b/res/drawable-xhdpi/ic_resolver_default.png deleted file mode 100644 index 61a428ebf..000000000 Binary files a/res/drawable-xhdpi/ic_resolver_default.png and /dev/null differ diff --git a/res/drawable-xhdpi/list_activated_holo.9.png b/res/drawable-xhdpi/list_activated_holo.9.png deleted file mode 100644 index d14fdb63d..000000000 Binary files a/res/drawable-xhdpi/list_activated_holo.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/list_focused_holo.9.png b/res/drawable-xhdpi/list_focused_holo.9.png deleted file mode 100644 index 0f773cabe..000000000 Binary files a/res/drawable-xhdpi/list_focused_holo.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/list_longpressed_holo.9.png b/res/drawable-xhdpi/list_longpressed_holo.9.png deleted file mode 100644 index d14fdb63d..000000000 Binary files a/res/drawable-xhdpi/list_longpressed_holo.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/list_pressed_holo_dark.9.png b/res/drawable-xhdpi/list_pressed_holo_dark.9.png deleted file mode 100644 index 5c48d7a9a..000000000 Binary files a/res/drawable-xhdpi/list_pressed_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png b/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png deleted file mode 100644 index 88726b691..000000000 Binary files a/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/menu_dropdown_panel_tomahawk.9.png b/res/drawable-xhdpi/menu_dropdown_panel_tomahawk.9.png deleted file mode 100755 index 067ef40ff..000000000 Binary files a/res/drawable-xhdpi/menu_dropdown_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/menu_hardkey_panel_tomahawk.9.png b/res/drawable-xhdpi/menu_hardkey_panel_tomahawk.9.png deleted file mode 100755 index 76ad874c1..000000000 Binary files a/res/drawable-xhdpi/menu_hardkey_panel_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/no_album_art_placeholder.png b/res/drawable-xhdpi/no_album_art_placeholder.png deleted file mode 100644 index 48c1bca5e..000000000 Binary files a/res/drawable-xhdpi/no_album_art_placeholder.png and /dev/null differ diff --git a/res/drawable-xhdpi/progress_bg_tomahawk.9.png b/res/drawable-xhdpi/progress_bg_tomahawk.9.png deleted file mode 100644 index 8b4853aa6..000000000 Binary files a/res/drawable-xhdpi/progress_bg_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/progress_indeterminate_drawable_tomahawk.png b/res/drawable-xhdpi/progress_indeterminate_drawable_tomahawk.png deleted file mode 100644 index 255bcd210..000000000 Binary files a/res/drawable-xhdpi/progress_indeterminate_drawable_tomahawk.png and /dev/null differ diff --git a/res/drawable-xhdpi/progress_primary_tomahawk.9.png b/res/drawable-xhdpi/progress_primary_tomahawk.9.png deleted file mode 100644 index 43fd95792..000000000 Binary files a/res/drawable-xhdpi/progress_primary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/progress_secondary_tomahawk.9.png b/res/drawable-xhdpi/progress_secondary_tomahawk.9.png deleted file mode 100644 index 460f5335f..000000000 Binary files a/res/drawable-xhdpi/progress_secondary_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/progress_thumb_tomahawk.png b/res/drawable-xhdpi/progress_thumb_tomahawk.png deleted file mode 100644 index a03470e72..000000000 Binary files a/res/drawable-xhdpi/progress_thumb_tomahawk.png and /dev/null differ diff --git a/res/drawable-xhdpi/spinner_ab_default_tomahawk.9.png b/res/drawable-xhdpi/spinner_ab_default_tomahawk.9.png deleted file mode 100644 index 14b1401de..000000000 Binary files a/res/drawable-xhdpi/spinner_ab_default_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/spinner_ab_disabled_tomahawk.9.png b/res/drawable-xhdpi/spinner_ab_disabled_tomahawk.9.png deleted file mode 100644 index c9dfbd605..000000000 Binary files a/res/drawable-xhdpi/spinner_ab_disabled_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/spinner_ab_focused_tomahawk.9.png b/res/drawable-xhdpi/spinner_ab_focused_tomahawk.9.png deleted file mode 100644 index c641bfabb..000000000 Binary files a/res/drawable-xhdpi/spinner_ab_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/spinner_ab_pressed_tomahawk.9.png b/res/drawable-xhdpi/spinner_ab_pressed_tomahawk.9.png deleted file mode 100644 index 446ab6779..000000000 Binary files a/res/drawable-xhdpi/spinner_ab_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/tab_selected_focused_tomahawk.9.png b/res/drawable-xhdpi/tab_selected_focused_tomahawk.9.png deleted file mode 100644 index 4b58252af..000000000 Binary files a/res/drawable-xhdpi/tab_selected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/tab_selected_pressed_tomahawk.9.png b/res/drawable-xhdpi/tab_selected_pressed_tomahawk.9.png deleted file mode 100644 index 254292e0f..000000000 Binary files a/res/drawable-xhdpi/tab_selected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/tab_selected_tomahawk.9.png b/res/drawable-xhdpi/tab_selected_tomahawk.9.png deleted file mode 100644 index d09eb864f..000000000 Binary files a/res/drawable-xhdpi/tab_selected_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/tab_unselected_focused_tomahawk.9.png b/res/drawable-xhdpi/tab_unselected_focused_tomahawk.9.png deleted file mode 100644 index c7d0d99f0..000000000 Binary files a/res/drawable-xhdpi/tab_unselected_focused_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/tab_unselected_pressed_tomahawk.9.png b/res/drawable-xhdpi/tab_unselected_pressed_tomahawk.9.png deleted file mode 100644 index d3a67321a..000000000 Binary files a/res/drawable-xhdpi/tab_unselected_pressed_tomahawk.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/textfield_activated_holo_dark.9.png b/res/drawable-xhdpi/textfield_activated_holo_dark.9.png deleted file mode 100644 index 78163f7b2..000000000 Binary files a/res/drawable-xhdpi/textfield_activated_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/textfield_default_holo_dark.9.png b/res/drawable-xhdpi/textfield_default_holo_dark.9.png deleted file mode 100644 index bc6a1771c..000000000 Binary files a/res/drawable-xhdpi/textfield_default_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/textfield_disabled_focused_holo_dark.9.png b/res/drawable-xhdpi/textfield_disabled_focused_holo_dark.9.png deleted file mode 100644 index 16be83999..000000000 Binary files a/res/drawable-xhdpi/textfield_disabled_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/textfield_disabled_holo_dark.9.png b/res/drawable-xhdpi/textfield_disabled_holo_dark.9.png deleted file mode 100644 index 3d143b9d1..000000000 Binary files a/res/drawable-xhdpi/textfield_disabled_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xhdpi/textfield_focused_holo_dark.9.png b/res/drawable-xhdpi/textfield_focused_holo_dark.9.png deleted file mode 100644 index bdfa02f83..000000000 Binary files a/res/drawable-xhdpi/textfield_focused_holo_dark.9.png and /dev/null differ diff --git a/res/drawable-xxhdpi/ic_launcher.png b/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index b78e543c6..000000000 Binary files a/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/res/drawable/abs__ic_menu_moreoverflow_holo_dark.xml b/res/drawable/abs__ic_menu_moreoverflow_holo_dark.xml deleted file mode 100644 index 2588a492d..000000000 --- a/res/drawable/abs__ic_menu_moreoverflow_holo_dark.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/res/drawable/activated_background_holo_dark.xml b/res/drawable/activated_background_holo_dark.xml deleted file mode 100644 index 5741ebab8..000000000 --- a/res/drawable/activated_background_holo_dark.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - diff --git a/res/drawable/edit_text_holo_dark.xml b/res/drawable/edit_text_holo_dark.xml deleted file mode 100644 index 05a570c90..000000000 --- a/res/drawable/edit_text_holo_dark.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/res/drawable/list_selector_background_transition_holo_dark.xml b/res/drawable/list_selector_background_transition_holo_dark.xml deleted file mode 100644 index a22572984..000000000 --- a/res/drawable/list_selector_background_transition_holo_dark.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - diff --git a/res/drawable/list_selector_holo_dark.xml b/res/drawable/list_selector_holo_dark.xml deleted file mode 100644 index cfe9487c5..000000000 --- a/res/drawable/list_selector_holo_dark.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/drawable/pressed_background_playback_large_land_tomahawk.xml b/res/drawable/pressed_background_playback_large_land_tomahawk.xml deleted file mode 100644 index c1ca715d3..000000000 --- a/res/drawable/pressed_background_playback_large_land_tomahawk.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/drawable/pressed_background_playback_large_tomahawk.xml b/res/drawable/pressed_background_playback_large_tomahawk.xml deleted file mode 100644 index d92ab07d0..000000000 --- a/res/drawable/pressed_background_playback_large_tomahawk.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/drawable/pressed_background_playback_small_land_tomahawk.xml b/res/drawable/pressed_background_playback_small_land_tomahawk.xml deleted file mode 100644 index d6c5e44a4..000000000 --- a/res/drawable/pressed_background_playback_small_land_tomahawk.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/drawable/pressed_background_playback_small_tomahawk.xml b/res/drawable/pressed_background_playback_small_tomahawk.xml deleted file mode 100644 index ac1185f69..000000000 --- a/res/drawable/pressed_background_playback_small_tomahawk.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/drawable/pressed_background_tomahawk.xml b/res/drawable/pressed_background_tomahawk.xml deleted file mode 100644 index d2295bedc..000000000 --- a/res/drawable/pressed_background_tomahawk.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/res/drawable/progress_horizontal_tomahawk.xml b/res/drawable/progress_horizontal_tomahawk.xml deleted file mode 100644 index 1c876612e..000000000 --- a/res/drawable/progress_horizontal_tomahawk.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/res/drawable/progress_indeterminate_tomahawk.xml b/res/drawable/progress_indeterminate_tomahawk.xml deleted file mode 100644 index 065348034..000000000 --- a/res/drawable/progress_indeterminate_tomahawk.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - \ No newline at end of file diff --git a/res/drawable/selectable_background_playback_large_land_tomahawk.xml b/res/drawable/selectable_background_playback_large_land_tomahawk.xml deleted file mode 100644 index d4a5e929d..000000000 --- a/res/drawable/selectable_background_playback_large_land_tomahawk.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/selectable_background_playback_large_tomahawk.xml b/res/drawable/selectable_background_playback_large_tomahawk.xml deleted file mode 100644 index 5ab975459..000000000 --- a/res/drawable/selectable_background_playback_large_tomahawk.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/selectable_background_playback_small_land_tomahawk.xml b/res/drawable/selectable_background_playback_small_land_tomahawk.xml deleted file mode 100644 index c15dcb4d1..000000000 --- a/res/drawable/selectable_background_playback_small_land_tomahawk.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/selectable_background_playback_small_tomahawk.xml b/res/drawable/selectable_background_playback_small_tomahawk.xml deleted file mode 100644 index c7956d423..000000000 --- a/res/drawable/selectable_background_playback_small_tomahawk.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/drawable/selectable_background_tomahawk_griditem.xml b/res/drawable/selectable_background_tomahawk_griditem.xml deleted file mode 100644 index d921b6e01..000000000 --- a/res/drawable/selectable_background_tomahawk_griditem.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/drawable/spinner_background_ab_tomahawk.xml b/res/drawable/spinner_background_ab_tomahawk.xml deleted file mode 100644 index d2401e57e..000000000 --- a/res/drawable/spinner_background_ab_tomahawk.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - diff --git a/res/drawable/tab_indicator_ab_tomahawk.xml b/res/drawable/tab_indicator_ab_tomahawk.xml deleted file mode 100644 index 214ef4843..000000000 --- a/res/drawable/tab_indicator_ab_tomahawk.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout-land/playback_fragment.xml b/res/layout-land/playback_fragment.xml deleted file mode 100644 index 085ad9f21..000000000 --- a/res/layout-land/playback_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-large-land/playback_fragment.xml b/res/layout-large-land/playback_fragment.xml deleted file mode 100644 index 7b53dc615..000000000 --- a/res/layout-large-land/playback_fragment.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-large/now_playing.xml b/res/layout-large/now_playing.xml deleted file mode 100644 index f27a33f5a..000000000 --- a/res/layout-large/now_playing.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-large/playback_fragment.xml b/res/layout-large/playback_fragment.xml deleted file mode 100644 index 2e81aee75..000000000 --- a/res/layout-large/playback_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-small-land/playback_fragment.xml b/res/layout-small-land/playback_fragment.xml deleted file mode 100644 index 00aa69b7f..000000000 --- a/res/layout-small-land/playback_fragment.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-small/playback_fragment.xml b/res/layout-small/playback_fragment.xml deleted file mode 100644 index 26f50963c..000000000 --- a/res/layout-small/playback_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-xlarge-land/playback_fragment.xml b/res/layout-xlarge-land/playback_fragment.xml deleted file mode 100644 index e6fd46849..000000000 --- a/res/layout-xlarge-land/playback_fragment.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-xlarge/now_playing.xml b/res/layout-xlarge/now_playing.xml deleted file mode 100644 index f27a33f5a..000000000 --- a/res/layout-xlarge/now_playing.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout-xlarge/playback_fragment.xml b/res/layout-xlarge/playback_fragment.xml deleted file mode 100644 index 51bc32f8d..000000000 --- a/res/layout-xlarge/playback_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/album_art_grid_item.xml b/res/layout/album_art_grid_item.xml deleted file mode 100644 index ca1e624fd..000000000 --- a/res/layout/album_art_grid_item.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/collapsible_edittext.xml b/res/layout/collapsible_edittext.xml deleted file mode 100644 index c4e9cc58b..000000000 --- a/res/layout/collapsible_edittext.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/res/layout/collection_activity.xml b/res/layout/collection_activity.xml deleted file mode 100644 index 24cf23d59..000000000 --- a/res/layout/collection_activity.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - diff --git a/res/layout/content_header.xml b/res/layout/content_header.xml deleted file mode 100644 index e20ff10df..000000000 --- a/res/layout/content_header.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/double_line_list_item.xml b/res/layout/double_line_list_item.xml deleted file mode 100644 index cbb902da4..000000000 --- a/res/layout/double_line_list_item.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/res/layout/double_line_list_item_with_image.xml b/res/layout/double_line_list_item_with_image.xml deleted file mode 100644 index d30e40299..000000000 --- a/res/layout/double_line_list_item_with_image.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/double_line_list_item_with_playstate_image.xml b/res/layout/double_line_list_item_with_playstate_image.xml deleted file mode 100644 index 8589eb4be..000000000 --- a/res/layout/double_line_list_item_with_playstate_image.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/fragment_container_list_item.xml b/res/layout/fragment_container_list_item.xml deleted file mode 100644 index fd7609cfd..000000000 --- a/res/layout/fragment_container_list_item.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/layout/login_activity.xml b/res/layout/login_activity.xml deleted file mode 100644 index 84d0086b8..000000000 --- a/res/layout/login_activity.xml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - -

>>0?1:0);T=m.low=T+J;m.high=fa+W+(T>>>0>>0?1: +0);U=N.low=U+K;N.high=ga+X+(U>>>0>>0?1:0);V=c.low=V+L;c.high=ha+Z+(V>>>0>>0?1:0)},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,f=8*a.sigBytes;b[f>>>5]|=128<<24-f%32;b[(f+128>>>10<<5)+30]=Math.floor(c/4294967296);b[(f+128>>>10<<5)+31]=c;a.sigBytes=4*b.length;this._process();return this._hash.toX32()},clone:function(){var a=c.clone.call(this);a._hash=this._hash.clone();return a},blockSize:32});j.SHA512=c._createHelper(b);j.HmacSHA512=c._createHmacHelper(b)})(); +(function(){var a=CryptoJS,j=a.enc.Utf8;a.algo.HMAC=a.lib.Base.extend({init:function(a,b){a=this._hasher=new a.init;"string"==typeof b&&(b=j.parse(b));var f=a.blockSize,l=4*f;b.sigBytes>l&&(b=a.finalize(b));b.clamp();for(var u=this._oKey=b.clone(),k=this._iKey=b.clone(),m=u.words,y=k.words,z=0;z>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<< +32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>3]|=parseInt(b.substr(j, +2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, +g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]), +c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c, +d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d, +C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/ +4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math); diff --git a/app/src/main/assets/js/cryptojs/pbkdf2.js b/app/src/main/assets/js/cryptojs/pbkdf2.js new file mode 100644 index 000000000..90b803f2b --- /dev/null +++ b/app/src/main/assets/js/cryptojs/pbkdf2.js @@ -0,0 +1,19 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(g,j){var e={},d=e.lib={},m=function(){},n=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=d.WordArray=n.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=j?c:4*a.length},toString:function(a){return(a||l).stringify(this)},concat:function(a){var c=this.words,p=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b>>2]|=(p[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=p[b>>>2];else c.push.apply(c,p);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=n.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, +2),16)<<24-4*(f%8);return new q.init(b,c/2)}},k=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new q.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, +u=d.BufferedBlockAlgorithm=n.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,f=b.sigBytes,l=this.blockSize,e=f/(4*l),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*l;f=g.min(4*a,f);if(a){for(var h=0;ha;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(l<<5|l>>>27)+j+m[a];c=20>a?c+((k&h|~k&g)+1518500249):40>a?c+((k^h^g)+1859775393):60>a?c+((k&h|k&g|h&g)-1894007588):c+((k^h^ +g)-899497514);j=g;g=h;h=k<<30|k>>>2;k=l;l=c}b[0]=b[0]+l|0;b[1]=b[1]+k|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+j|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,l=8*d.sigBytes;e[l>>>5]|=128<<24-l%32;e[(l+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(l+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(j);g.HmacSHA1=d._createHmacHelper(j)})(); +(function(){var g=CryptoJS,j=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=j.parse(d));var g=e.blockSize,n=4*g;d.sigBytes>n&&(d=e.finalize(d));d.clamp();for(var q=this._oKey=d.clone(),b=this._iKey=d.clone(),l=q.words,k=b.words,h=0;h>>2]|=(m[r>>>2]>>>24-8*(r%4)&255)<<24-8*((n+r)%4);else if(65535>>2]=m[r>>>2];else b.push.apply(b,m);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<< +32-8*(b%4);a.length=q.ceil(b/4)},clone:function(){var a=c.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],m=0;m>>2]>>>24-8*(n%4)&255;m.push((r>>>4).toString(16));m.push((r&15).toString(16))}return m.join("")},parse:function(a){for(var b=a.length,m=[],n=0;n>>3]|=parseInt(a.substr(n, +2),16)<<24-4*(n%8);return new s.init(m,b/2)}},a=b.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var m=[],n=0;n>>2]>>>24-8*(n%4)&255));return m.join("")},parse:function(a){for(var b=a.length,m=[],n=0;n>>2]|=(a.charCodeAt(n)&255)<<24-8*(n%4);return new s.init(m,b)}},t=b.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, +u=l.BufferedBlockAlgorithm=c.extend({reset:function(){this._data=new s.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=t.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,m=b.words,n=b.sigBytes,r=this.blockSize,c=n/(4*r),c=a?q.ceil(c):q.max((c|0)-this._minBufferSize,0);a=c*r;n=q.min(4*a,n);if(a){for(var u=0;u>>2]>>>24-8*(k%4)&255)<<16|(l[k+1>>>2]>>>24-8*((k+1)%4)&255)<<8|l[k+2>>>2]>>>24-8*((k+2)%4)&255,d=0;4>d&&k+0.75*d>>6*(3-d)&63));if(l=c.charAt(64))for(;e.length%4;)e.push(l);return e.join("")},parse:function(e){var l=e.length,p=this._map,c=p.charAt(64);c&&(c=e.indexOf(c),-1!=c&&(l=c));for(var c=[],s=0,b=0;b< +l;b++)if(b%4){var d=p.indexOf(e.charAt(b-1))<<2*(b%4),a=p.indexOf(e.charAt(b))>>>6-2*(b%4);c[s>>>2]|=(d|a)<<24-8*(s%4);s++}return k.create(c,s)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(q){function k(a,b,c,d,m,n,r){a=a+(b&c|~b&d)+m+r;return(a<>>32-n)+b}function e(a,b,c,d,m,n,r){a=a+(b&d|c&~d)+m+r;return(a<>>32-n)+b}function l(a,b,c,d,m,n,r){a=a+(b^c^d)+m+r;return(a<>>32-n)+b}function p(a,b,c,d,m,n,r){a=a+(c^(b|~d))+m+r;return(a<>>32-n)+b}for(var c=CryptoJS,s=c.lib,b=s.WordArray,d=s.Hasher,s=c.algo,a=[],t=0;64>t;t++)a[t]=4294967296*q.abs(q.sin(t+1))|0;s=s.MD5=d.extend({_doReset:function(){this._hash=new b.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(b,c){for(var d=0;16>d;d++){var t=c+d,m=b[t];b[t]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360}var d=this._hash.words,t=b[c+0],m=b[c+1],n=b[c+2],r=b[c+3],x=b[c+4],s=b[c+5],q=b[c+6],y=b[c+7],z=b[c+8],A=b[c+9],B=b[c+10],C=b[c+11],D=b[c+12],E=b[c+13],F=b[c+14],G=b[c+15],f=d[0],g=d[1],h=d[2],j=d[3],f=k(f,g,h,j,t,7,a[0]),j=k(j,f,g,h,m,12,a[1]),h=k(h,j,f,g,n,17,a[2]),g=k(g,h,j,f,r,22,a[3]),f=k(f,g,h,j,x,7,a[4]),j=k(j,f,g,h,s,12,a[5]),h=k(h,j,f,g,q,17,a[6]),g=k(g,h,j,f,y,22,a[7]), +f=k(f,g,h,j,z,7,a[8]),j=k(j,f,g,h,A,12,a[9]),h=k(h,j,f,g,B,17,a[10]),g=k(g,h,j,f,C,22,a[11]),f=k(f,g,h,j,D,7,a[12]),j=k(j,f,g,h,E,12,a[13]),h=k(h,j,f,g,F,17,a[14]),g=k(g,h,j,f,G,22,a[15]),f=e(f,g,h,j,m,5,a[16]),j=e(j,f,g,h,q,9,a[17]),h=e(h,j,f,g,C,14,a[18]),g=e(g,h,j,f,t,20,a[19]),f=e(f,g,h,j,s,5,a[20]),j=e(j,f,g,h,B,9,a[21]),h=e(h,j,f,g,G,14,a[22]),g=e(g,h,j,f,x,20,a[23]),f=e(f,g,h,j,A,5,a[24]),j=e(j,f,g,h,F,9,a[25]),h=e(h,j,f,g,r,14,a[26]),g=e(g,h,j,f,z,20,a[27]),f=e(f,g,h,j,E,5,a[28]),j=e(j,f, +g,h,n,9,a[29]),h=e(h,j,f,g,y,14,a[30]),g=e(g,h,j,f,D,20,a[31]),f=l(f,g,h,j,s,4,a[32]),j=l(j,f,g,h,z,11,a[33]),h=l(h,j,f,g,C,16,a[34]),g=l(g,h,j,f,F,23,a[35]),f=l(f,g,h,j,m,4,a[36]),j=l(j,f,g,h,x,11,a[37]),h=l(h,j,f,g,y,16,a[38]),g=l(g,h,j,f,B,23,a[39]),f=l(f,g,h,j,E,4,a[40]),j=l(j,f,g,h,t,11,a[41]),h=l(h,j,f,g,r,16,a[42]),g=l(g,h,j,f,q,23,a[43]),f=l(f,g,h,j,A,4,a[44]),j=l(j,f,g,h,D,11,a[45]),h=l(h,j,f,g,G,16,a[46]),g=l(g,h,j,f,n,23,a[47]),f=p(f,g,h,j,t,6,a[48]),j=p(j,f,g,h,y,10,a[49]),h=p(h,j,f,g, +F,15,a[50]),g=p(g,h,j,f,s,21,a[51]),f=p(f,g,h,j,D,6,a[52]),j=p(j,f,g,h,r,10,a[53]),h=p(h,j,f,g,B,15,a[54]),g=p(g,h,j,f,m,21,a[55]),f=p(f,g,h,j,z,6,a[56]),j=p(j,f,g,h,G,10,a[57]),h=p(h,j,f,g,q,15,a[58]),g=p(g,h,j,f,E,21,a[59]),f=p(f,g,h,j,x,6,a[60]),j=p(j,f,g,h,C,10,a[61]),h=p(h,j,f,g,n,15,a[62]),g=p(g,h,j,f,A,21,a[63]);d[0]=d[0]+f|0;d[1]=d[1]+g|0;d[2]=d[2]+h|0;d[3]=d[3]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32;var m=q.floor(c/ +4294967296);b[(d+64>>>9<<4)+15]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360;b[(d+64>>>9<<4)+14]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;a.sigBytes=4*(b.length+1);this._process();a=this._hash;b=a.words;for(c=0;4>c;c++)d=b[c],b[c]=(d<<8|d>>>24)&16711935|(d<<24|d>>>8)&4278255360;return a},clone:function(){var a=d.clone.call(this);a._hash=this._hash.clone();return a}});c.MD5=d._createHelper(s);c.HmacMD5=d._createHmacHelper(s)})(Math); +(function(){var q=CryptoJS,k=q.lib,e=k.Base,l=k.WordArray,k=q.algo,p=k.EvpKDF=e.extend({cfg:e.extend({keySize:4,hasher:k.MD5,iterations:1}),init:function(c){this.cfg=this.cfg.extend(c)},compute:function(c,e){for(var b=this.cfg,d=b.hasher.create(),a=l.create(),k=a.words,p=b.keySize,b=b.iterations;k.length>>2]&255}};e.BlockCipher=d.extend({cfg:d.cfg.extend({mode:a,padding:u}),reset:function(){d.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, +this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var w=e.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),a=(k.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?p.create([1398893684, +1701076831]).concat(a).concat(b):b).toString(s)},parse:function(a){a=s.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=p.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return w.create({ciphertext:a,salt:c})}},v=e.SerializableCipher=l.extend({cfg:l.extend({format:a}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var e=a.createEncryptor(c,d);b=e.finalize(b);e=e.cfg;return w.create({ciphertext:b,key:c,iv:e.iv,algorithm:a,mode:e.mode,padding:e.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),k=(k.kdf={}).OpenSSL={execute:function(a,c,d,e){e||(e=p.random(8));a=b.create({keySize:c+d}).compute(a,e);d=p.create(a.words.slice(c),4*d);a.sigBytes=4*c;return w.create({key:a,iv:d,salt:e})}},H=e.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:k}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);c=d.kdf.execute(c, +a.keySize,a.ivSize);d.iv=c.iv;a=v.encrypt.call(this,a,b,c.key,d);a.mixIn(c);return a},decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);c=d.kdf.execute(c,a.keySize,a.ivSize,b.salt);d.iv=c.iv;return v.decrypt.call(this,a,b,c.key,d)}})}(); +(function(){function q(){for(var b=this._X,d=this._C,a=0;8>a;a++)p[a]=d[a];d[0]=d[0]+1295307597+this._b|0;d[1]=d[1]+3545052371+(d[0]>>>0>>0?1:0)|0;d[2]=d[2]+886263092+(d[1]>>>0>>0?1:0)|0;d[3]=d[3]+1295307597+(d[2]>>>0>>0?1:0)|0;d[4]=d[4]+3545052371+(d[3]>>>0>>0?1:0)|0;d[5]=d[5]+886263092+(d[4]>>>0>>0?1:0)|0;d[6]=d[6]+1295307597+(d[5]>>>0>>0?1:0)|0;d[7]=d[7]+3545052371+(d[6]>>>0>>0?1:0)|0;this._b=d[7]>>>0>>0?1:0;for(a=0;8>a;a++){var e=b[a]+d[a],k=e&65535, +l=e>>>16;c[a]=((k*k>>>17)+k*l>>>15)+l*l^((e&4294901760)*e|0)+((e&65535)*e|0)}b[0]=c[0]+(c[7]<<16|c[7]>>>16)+(c[6]<<16|c[6]>>>16)|0;b[1]=c[1]+(c[0]<<8|c[0]>>>24)+c[7]|0;b[2]=c[2]+(c[1]<<16|c[1]>>>16)+(c[0]<<16|c[0]>>>16)|0;b[3]=c[3]+(c[2]<<8|c[2]>>>24)+c[1]|0;b[4]=c[4]+(c[3]<<16|c[3]>>>16)+(c[2]<<16|c[2]>>>16)|0;b[5]=c[5]+(c[4]<<8|c[4]>>>24)+c[3]|0;b[6]=c[6]+(c[5]<<16|c[5]>>>16)+(c[4]<<16|c[4]>>>16)|0;b[7]=c[7]+(c[6]<<8|c[6]>>>24)+c[5]|0}var k=CryptoJS,e=k.lib.StreamCipher,l=[],p=[],c=[],s=k.algo.RabbitLegacy= +e.extend({_doReset:function(){for(var b=this._key.words,c=this.cfg.iv,a=this._X=[b[0],b[3]<<16|b[2]>>>16,b[1],b[0]<<16|b[3]>>>16,b[2],b[1]<<16|b[0]>>>16,b[3],b[2]<<16|b[1]>>>16],b=this._C=[b[2]<<16|b[2]>>>16,b[0]&4294901760|b[1]&65535,b[3]<<16|b[3]>>>16,b[1]&4294901760|b[2]&65535,b[0]<<16|b[0]>>>16,b[2]&4294901760|b[3]&65535,b[1]<<16|b[1]>>>16,b[3]&4294901760|b[0]&65535],e=this._b=0;4>e;e++)q.call(this);for(e=0;8>e;e++)b[e]^=a[e+4&7];if(c){var a=c.words,c=a[0],a=a[1],c=(c<<8|c>>>24)&16711935|(c<< +24|c>>>8)&4278255360,a=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360,e=c>>>16|a&4294901760,k=a<<16|c&65535;b[0]^=c;b[1]^=e;b[2]^=a;b[3]^=k;b[4]^=c;b[5]^=e;b[6]^=a;b[7]^=k;for(e=0;4>e;e++)q.call(this)}},_doProcessBlock:function(b,c){var a=this._X;q.call(this);l[0]=a[0]^a[5]>>>16^a[3]<<16;l[1]=a[2]^a[7]>>>16^a[5]<<16;l[2]=a[4]^a[1]>>>16^a[7]<<16;l[3]=a[6]^a[3]>>>16^a[1]<<16;for(a=0;4>a;a++)l[a]=(l[a]<<8|l[a]>>>24)&16711935|(l[a]<<24|l[a]>>>8)&4278255360,b[c+a]^=l[a]},blockSize:4,ivSize:2});k.RabbitLegacy= +e._createHelper(s)})(); diff --git a/app/src/main/assets/js/cryptojs/rabbit.js b/app/src/main/assets/js/cryptojs/rabbit.js new file mode 100644 index 000000000..75c6f570d --- /dev/null +++ b/app/src/main/assets/js/cryptojs/rabbit.js @@ -0,0 +1,36 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(q,k){var e={},l=e.lib={},p=function(){},c=l.Base={extend:function(a){p.prototype=this;var b=new p;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +s=l.WordArray=c.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=k?b:4*a.length},toString:function(a){return(a||d).stringify(this)},concat:function(a){var b=this.words,m=a.words,n=this.sigBytes;a=a.sigBytes;this.clamp();if(n%4)for(var r=0;r>>2]|=(m[r>>>2]>>>24-8*(r%4)&255)<<24-8*((n+r)%4);else if(65535>>2]=m[r>>>2];else b.push.apply(b,m);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<< +32-8*(b%4);a.length=q.ceil(b/4)},clone:function(){var a=c.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],m=0;m>>2]>>>24-8*(n%4)&255;m.push((r>>>4).toString(16));m.push((r&15).toString(16))}return m.join("")},parse:function(a){for(var b=a.length,m=[],n=0;n>>3]|=parseInt(a.substr(n, +2),16)<<24-4*(n%8);return new s.init(m,b/2)}},a=b.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var m=[],n=0;n>>2]>>>24-8*(n%4)&255));return m.join("")},parse:function(a){for(var b=a.length,m=[],n=0;n>>2]|=(a.charCodeAt(n)&255)<<24-8*(n%4);return new s.init(m,b)}},u=b.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, +t=l.BufferedBlockAlgorithm=c.extend({reset:function(){this._data=new s.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=u.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,m=b.words,n=b.sigBytes,r=this.blockSize,c=n/(4*r),c=a?q.ceil(c):q.max((c|0)-this._minBufferSize,0);a=c*r;n=q.min(4*a,n);if(a){for(var t=0;t>>2]>>>24-8*(k%4)&255)<<16|(l[k+1>>>2]>>>24-8*((k+1)%4)&255)<<8|l[k+2>>>2]>>>24-8*((k+2)%4)&255,d=0;4>d&&k+0.75*d>>6*(3-d)&63));if(l=c.charAt(64))for(;e.length%4;)e.push(l);return e.join("")},parse:function(e){var l=e.length,p=this._map,c=p.charAt(64);c&&(c=e.indexOf(c),-1!=c&&(l=c));for(var c=[],s=0,b=0;b< +l;b++)if(b%4){var d=p.indexOf(e.charAt(b-1))<<2*(b%4),a=p.indexOf(e.charAt(b))>>>6-2*(b%4);c[s>>>2]|=(d|a)<<24-8*(s%4);s++}return k.create(c,s)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(q){function k(a,b,c,d,m,n,r){a=a+(b&c|~b&d)+m+r;return(a<>>32-n)+b}function e(a,b,c,d,m,n,r){a=a+(b&d|c&~d)+m+r;return(a<>>32-n)+b}function l(a,b,c,d,m,n,r){a=a+(b^c^d)+m+r;return(a<>>32-n)+b}function p(a,b,c,d,m,n,r){a=a+(c^(b|~d))+m+r;return(a<>>32-n)+b}for(var c=CryptoJS,s=c.lib,b=s.WordArray,d=s.Hasher,s=c.algo,a=[],u=0;64>u;u++)a[u]=4294967296*q.abs(q.sin(u+1))|0;s=s.MD5=d.extend({_doReset:function(){this._hash=new b.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(b,c){for(var d=0;16>d;d++){var s=c+d,m=b[s];b[s]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360}var d=this._hash.words,s=b[c+0],m=b[c+1],n=b[c+2],r=b[c+3],x=b[c+4],u=b[c+5],q=b[c+6],y=b[c+7],z=b[c+8],A=b[c+9],B=b[c+10],C=b[c+11],D=b[c+12],E=b[c+13],F=b[c+14],G=b[c+15],f=d[0],g=d[1],h=d[2],j=d[3],f=k(f,g,h,j,s,7,a[0]),j=k(j,f,g,h,m,12,a[1]),h=k(h,j,f,g,n,17,a[2]),g=k(g,h,j,f,r,22,a[3]),f=k(f,g,h,j,x,7,a[4]),j=k(j,f,g,h,u,12,a[5]),h=k(h,j,f,g,q,17,a[6]),g=k(g,h,j,f,y,22,a[7]), +f=k(f,g,h,j,z,7,a[8]),j=k(j,f,g,h,A,12,a[9]),h=k(h,j,f,g,B,17,a[10]),g=k(g,h,j,f,C,22,a[11]),f=k(f,g,h,j,D,7,a[12]),j=k(j,f,g,h,E,12,a[13]),h=k(h,j,f,g,F,17,a[14]),g=k(g,h,j,f,G,22,a[15]),f=e(f,g,h,j,m,5,a[16]),j=e(j,f,g,h,q,9,a[17]),h=e(h,j,f,g,C,14,a[18]),g=e(g,h,j,f,s,20,a[19]),f=e(f,g,h,j,u,5,a[20]),j=e(j,f,g,h,B,9,a[21]),h=e(h,j,f,g,G,14,a[22]),g=e(g,h,j,f,x,20,a[23]),f=e(f,g,h,j,A,5,a[24]),j=e(j,f,g,h,F,9,a[25]),h=e(h,j,f,g,r,14,a[26]),g=e(g,h,j,f,z,20,a[27]),f=e(f,g,h,j,E,5,a[28]),j=e(j,f, +g,h,n,9,a[29]),h=e(h,j,f,g,y,14,a[30]),g=e(g,h,j,f,D,20,a[31]),f=l(f,g,h,j,u,4,a[32]),j=l(j,f,g,h,z,11,a[33]),h=l(h,j,f,g,C,16,a[34]),g=l(g,h,j,f,F,23,a[35]),f=l(f,g,h,j,m,4,a[36]),j=l(j,f,g,h,x,11,a[37]),h=l(h,j,f,g,y,16,a[38]),g=l(g,h,j,f,B,23,a[39]),f=l(f,g,h,j,E,4,a[40]),j=l(j,f,g,h,s,11,a[41]),h=l(h,j,f,g,r,16,a[42]),g=l(g,h,j,f,q,23,a[43]),f=l(f,g,h,j,A,4,a[44]),j=l(j,f,g,h,D,11,a[45]),h=l(h,j,f,g,G,16,a[46]),g=l(g,h,j,f,n,23,a[47]),f=p(f,g,h,j,s,6,a[48]),j=p(j,f,g,h,y,10,a[49]),h=p(h,j,f,g, +F,15,a[50]),g=p(g,h,j,f,u,21,a[51]),f=p(f,g,h,j,D,6,a[52]),j=p(j,f,g,h,r,10,a[53]),h=p(h,j,f,g,B,15,a[54]),g=p(g,h,j,f,m,21,a[55]),f=p(f,g,h,j,z,6,a[56]),j=p(j,f,g,h,G,10,a[57]),h=p(h,j,f,g,q,15,a[58]),g=p(g,h,j,f,E,21,a[59]),f=p(f,g,h,j,x,6,a[60]),j=p(j,f,g,h,C,10,a[61]),h=p(h,j,f,g,n,15,a[62]),g=p(g,h,j,f,A,21,a[63]);d[0]=d[0]+f|0;d[1]=d[1]+g|0;d[2]=d[2]+h|0;d[3]=d[3]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32;var m=q.floor(c/ +4294967296);b[(d+64>>>9<<4)+15]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360;b[(d+64>>>9<<4)+14]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;a.sigBytes=4*(b.length+1);this._process();a=this._hash;b=a.words;for(c=0;4>c;c++)d=b[c],b[c]=(d<<8|d>>>24)&16711935|(d<<24|d>>>8)&4278255360;return a},clone:function(){var a=d.clone.call(this);a._hash=this._hash.clone();return a}});c.MD5=d._createHelper(s);c.HmacMD5=d._createHmacHelper(s)})(Math); +(function(){var q=CryptoJS,k=q.lib,e=k.Base,l=k.WordArray,k=q.algo,p=k.EvpKDF=e.extend({cfg:e.extend({keySize:4,hasher:k.MD5,iterations:1}),init:function(c){this.cfg=this.cfg.extend(c)},compute:function(c,e){for(var b=this.cfg,d=b.hasher.create(),a=l.create(),k=a.words,p=b.keySize,b=b.iterations;k.length>>2]&255}};e.BlockCipher=d.extend({cfg:d.cfg.extend({mode:a,padding:t}),reset:function(){d.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, +this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var w=e.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),a=(k.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?p.create([1398893684, +1701076831]).concat(a).concat(b):b).toString(s)},parse:function(a){a=s.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=p.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return w.create({ciphertext:a,salt:c})}},v=e.SerializableCipher=l.extend({cfg:l.extend({format:a}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var e=a.createEncryptor(c,d);b=e.finalize(b);e=e.cfg;return w.create({ciphertext:b,key:c,iv:e.iv,algorithm:a,mode:e.mode,padding:e.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),k=(k.kdf={}).OpenSSL={execute:function(a,c,d,e){e||(e=p.random(8));a=b.create({keySize:c+d}).compute(a,e);d=p.create(a.words.slice(c),4*d);a.sigBytes=4*c;return w.create({key:a,iv:d,salt:e})}},H=e.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:k}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);c=d.kdf.execute(c, +a.keySize,a.ivSize);d.iv=c.iv;a=v.encrypt.call(this,a,b,c.key,d);a.mixIn(c);return a},decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);c=d.kdf.execute(c,a.keySize,a.ivSize,b.salt);d.iv=c.iv;return v.decrypt.call(this,a,b,c.key,d)}})}(); +(function(){function q(){for(var b=this._X,d=this._C,a=0;8>a;a++)p[a]=d[a];d[0]=d[0]+1295307597+this._b|0;d[1]=d[1]+3545052371+(d[0]>>>0>>0?1:0)|0;d[2]=d[2]+886263092+(d[1]>>>0>>0?1:0)|0;d[3]=d[3]+1295307597+(d[2]>>>0>>0?1:0)|0;d[4]=d[4]+3545052371+(d[3]>>>0>>0?1:0)|0;d[5]=d[5]+886263092+(d[4]>>>0>>0?1:0)|0;d[6]=d[6]+1295307597+(d[5]>>>0>>0?1:0)|0;d[7]=d[7]+3545052371+(d[6]>>>0>>0?1:0)|0;this._b=d[7]>>>0>>0?1:0;for(a=0;8>a;a++){var e=b[a]+d[a],k=e&65535, +l=e>>>16;c[a]=((k*k>>>17)+k*l>>>15)+l*l^((e&4294901760)*e|0)+((e&65535)*e|0)}b[0]=c[0]+(c[7]<<16|c[7]>>>16)+(c[6]<<16|c[6]>>>16)|0;b[1]=c[1]+(c[0]<<8|c[0]>>>24)+c[7]|0;b[2]=c[2]+(c[1]<<16|c[1]>>>16)+(c[0]<<16|c[0]>>>16)|0;b[3]=c[3]+(c[2]<<8|c[2]>>>24)+c[1]|0;b[4]=c[4]+(c[3]<<16|c[3]>>>16)+(c[2]<<16|c[2]>>>16)|0;b[5]=c[5]+(c[4]<<8|c[4]>>>24)+c[3]|0;b[6]=c[6]+(c[5]<<16|c[5]>>>16)+(c[4]<<16|c[4]>>>16)|0;b[7]=c[7]+(c[6]<<8|c[6]>>>24)+c[5]|0}var k=CryptoJS,e=k.lib.StreamCipher,l=[],p=[],c=[],s=k.algo.Rabbit= +e.extend({_doReset:function(){for(var b=this._key.words,c=this.cfg.iv,a=0;4>a;a++)b[a]=(b[a]<<8|b[a]>>>24)&16711935|(b[a]<<24|b[a]>>>8)&4278255360;for(var e=this._X=[b[0],b[3]<<16|b[2]>>>16,b[1],b[0]<<16|b[3]>>>16,b[2],b[1]<<16|b[0]>>>16,b[3],b[2]<<16|b[1]>>>16],b=this._C=[b[2]<<16|b[2]>>>16,b[0]&4294901760|b[1]&65535,b[3]<<16|b[3]>>>16,b[1]&4294901760|b[2]&65535,b[0]<<16|b[0]>>>16,b[2]&4294901760|b[3]&65535,b[1]<<16|b[1]>>>16,b[3]&4294901760|b[0]&65535],a=this._b=0;4>a;a++)q.call(this);for(a=0;8> +a;a++)b[a]^=e[a+4&7];if(c){var a=c.words,c=a[0],a=a[1],c=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360,a=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360,e=c>>>16|a&4294901760,k=a<<16|c&65535;b[0]^=c;b[1]^=e;b[2]^=a;b[3]^=k;b[4]^=c;b[5]^=e;b[6]^=a;b[7]^=k;for(a=0;4>a;a++)q.call(this)}},_doProcessBlock:function(b,c){var a=this._X;q.call(this);l[0]=a[0]^a[5]>>>16^a[3]<<16;l[1]=a[2]^a[7]>>>16^a[5]<<16;l[2]=a[4]^a[1]>>>16^a[7]<<16;l[3]=a[6]^a[3]>>>16^a[1]<<16;for(a=0;4>a;a++)l[a]=(l[a]<<8|l[a]>>>24)& +16711935|(l[a]<<24|l[a]>>>8)&4278255360,b[c+a]^=l[a]},blockSize:4,ivSize:2});k.Rabbit=e._createHelper(s)})(); diff --git a/app/src/main/assets/js/cryptojs/rc4.js b/app/src/main/assets/js/cryptojs/rc4.js new file mode 100644 index 000000000..33a0ed64d --- /dev/null +++ b/app/src/main/assets/js/cryptojs/rc4.js @@ -0,0 +1,33 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(s,l){var e={},n=e.lib={},p=function(){},b=n.Base={extend:function(c){p.prototype=this;var a=new p;c&&a.mixIn(c);a.hasOwnProperty("init")||(a.init=function(){a.$super.init.apply(this,arguments)});a.init.prototype=a;a.$super=this;return a},create:function(){var c=this.extend();c.init.apply(c,arguments);return c},init:function(){},mixIn:function(c){for(var a in c)c.hasOwnProperty(a)&&(this[a]=c[a]);c.hasOwnProperty("toString")&&(this.toString=c.toString)},clone:function(){return this.init.prototype.extend(this)}}, +d=n.WordArray=b.extend({init:function(c,a){c=this.words=c||[];this.sigBytes=a!=l?a:4*c.length},toString:function(c){return(c||q).stringify(this)},concat:function(c){var a=this.words,m=c.words,f=this.sigBytes;c=c.sigBytes;this.clamp();if(f%4)for(var r=0;r>>2]|=(m[r>>>2]>>>24-8*(r%4)&255)<<24-8*((f+r)%4);else if(65535>>2]=m[r>>>2];else a.push.apply(a,m);this.sigBytes+=c;return this},clamp:function(){var c=this.words,a=this.sigBytes;c[a>>>2]&=4294967295<< +32-8*(a%4);c.length=s.ceil(a/4)},clone:function(){var c=b.clone.call(this);c.words=this.words.slice(0);return c},random:function(c){for(var a=[],m=0;m>>2]>>>24-8*(f%4)&255;m.push((r>>>4).toString(16));m.push((r&15).toString(16))}return m.join("")},parse:function(c){for(var a=c.length,m=[],f=0;f>>3]|=parseInt(c.substr(f, +2),16)<<24-4*(f%8);return new d.init(m,a/2)}},a=t.Latin1={stringify:function(c){var a=c.words;c=c.sigBytes;for(var m=[],f=0;f>>2]>>>24-8*(f%4)&255));return m.join("")},parse:function(c){for(var a=c.length,m=[],f=0;f>>2]|=(c.charCodeAt(f)&255)<<24-8*(f%4);return new d.init(m,a)}},v=t.Utf8={stringify:function(c){try{return decodeURIComponent(escape(a.stringify(c)))}catch(u){throw Error("Malformed UTF-8 data");}},parse:function(c){return a.parse(unescape(encodeURIComponent(c)))}}, +u=n.BufferedBlockAlgorithm=b.extend({reset:function(){this._data=new d.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=v.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var u=this._data,m=u.words,f=u.sigBytes,r=this.blockSize,e=f/(4*r),e=a?s.ceil(e):s.max((e|0)-this._minBufferSize,0);a=e*r;f=s.min(4*a,f);if(a){for(var b=0;b>>2]>>>24-8*(d%4)&255)<<16|(n[d+1>>>2]>>>24-8*((d+1)%4)&255)<<8|n[d+2>>>2]>>>24-8*((d+2)%4)&255,q=0;4>q&&d+0.75*q>>6*(3-q)&63));if(n=b.charAt(64))for(;e.length%4;)e.push(n);return e.join("")},parse:function(e){var n=e.length,p=this._map,b=p.charAt(64);b&&(b=e.indexOf(b),-1!=b&&(n=b));for(var b=[],d=0,t=0;t< +n;t++)if(t%4){var q=p.indexOf(e.charAt(t-1))<<2*(t%4),a=p.indexOf(e.charAt(t))>>>6-2*(t%4);b[d>>>2]|=(q|a)<<24-8*(d%4);d++}return l.create(b,d)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(s){function l(a,b,c,e,m,f,r){a=a+(b&c|~b&e)+m+r;return(a<>>32-f)+b}function e(a,b,c,e,m,f,r){a=a+(b&e|c&~e)+m+r;return(a<>>32-f)+b}function n(a,b,c,e,m,f,r){a=a+(b^c^e)+m+r;return(a<>>32-f)+b}function p(a,b,c,e,m,f,r){a=a+(c^(b|~e))+m+r;return(a<>>32-f)+b}for(var b=CryptoJS,d=b.lib,t=d.WordArray,q=d.Hasher,d=b.algo,a=[],v=0;64>v;v++)a[v]=4294967296*s.abs(s.sin(v+1))|0;d=d.MD5=q.extend({_doReset:function(){this._hash=new t.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(b,d){for(var c=0;16>c;c++){var q=d+c,m=b[q];b[q]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360}var c=this._hash.words,q=b[d+0],m=b[d+1],f=b[d+2],r=b[d+3],x=b[d+4],t=b[d+5],s=b[d+6],v=b[d+7],y=b[d+8],z=b[d+9],A=b[d+10],B=b[d+11],C=b[d+12],D=b[d+13],E=b[d+14],F=b[d+15],g=c[0],h=c[1],j=c[2],k=c[3],g=l(g,h,j,k,q,7,a[0]),k=l(k,g,h,j,m,12,a[1]),j=l(j,k,g,h,f,17,a[2]),h=l(h,j,k,g,r,22,a[3]),g=l(g,h,j,k,x,7,a[4]),k=l(k,g,h,j,t,12,a[5]),j=l(j,k,g,h,s,17,a[6]),h=l(h,j,k,g,v,22,a[7]), +g=l(g,h,j,k,y,7,a[8]),k=l(k,g,h,j,z,12,a[9]),j=l(j,k,g,h,A,17,a[10]),h=l(h,j,k,g,B,22,a[11]),g=l(g,h,j,k,C,7,a[12]),k=l(k,g,h,j,D,12,a[13]),j=l(j,k,g,h,E,17,a[14]),h=l(h,j,k,g,F,22,a[15]),g=e(g,h,j,k,m,5,a[16]),k=e(k,g,h,j,s,9,a[17]),j=e(j,k,g,h,B,14,a[18]),h=e(h,j,k,g,q,20,a[19]),g=e(g,h,j,k,t,5,a[20]),k=e(k,g,h,j,A,9,a[21]),j=e(j,k,g,h,F,14,a[22]),h=e(h,j,k,g,x,20,a[23]),g=e(g,h,j,k,z,5,a[24]),k=e(k,g,h,j,E,9,a[25]),j=e(j,k,g,h,r,14,a[26]),h=e(h,j,k,g,y,20,a[27]),g=e(g,h,j,k,D,5,a[28]),k=e(k,g, +h,j,f,9,a[29]),j=e(j,k,g,h,v,14,a[30]),h=e(h,j,k,g,C,20,a[31]),g=n(g,h,j,k,t,4,a[32]),k=n(k,g,h,j,y,11,a[33]),j=n(j,k,g,h,B,16,a[34]),h=n(h,j,k,g,E,23,a[35]),g=n(g,h,j,k,m,4,a[36]),k=n(k,g,h,j,x,11,a[37]),j=n(j,k,g,h,v,16,a[38]),h=n(h,j,k,g,A,23,a[39]),g=n(g,h,j,k,D,4,a[40]),k=n(k,g,h,j,q,11,a[41]),j=n(j,k,g,h,r,16,a[42]),h=n(h,j,k,g,s,23,a[43]),g=n(g,h,j,k,z,4,a[44]),k=n(k,g,h,j,C,11,a[45]),j=n(j,k,g,h,F,16,a[46]),h=n(h,j,k,g,f,23,a[47]),g=p(g,h,j,k,q,6,a[48]),k=p(k,g,h,j,v,10,a[49]),j=p(j,k,g,h, +E,15,a[50]),h=p(h,j,k,g,t,21,a[51]),g=p(g,h,j,k,C,6,a[52]),k=p(k,g,h,j,r,10,a[53]),j=p(j,k,g,h,A,15,a[54]),h=p(h,j,k,g,m,21,a[55]),g=p(g,h,j,k,y,6,a[56]),k=p(k,g,h,j,F,10,a[57]),j=p(j,k,g,h,s,15,a[58]),h=p(h,j,k,g,D,21,a[59]),g=p(g,h,j,k,x,6,a[60]),k=p(k,g,h,j,B,10,a[61]),j=p(j,k,g,h,f,15,a[62]),h=p(h,j,k,g,z,21,a[63]);c[0]=c[0]+g|0;c[1]=c[1]+h|0;c[2]=c[2]+j|0;c[3]=c[3]+k|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32;var m=s.floor(c/ +4294967296);b[(d+64>>>9<<4)+15]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360;b[(d+64>>>9<<4)+14]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;a.sigBytes=4*(b.length+1);this._process();a=this._hash;b=a.words;for(c=0;4>c;c++)d=b[c],b[c]=(d<<8|d>>>24)&16711935|(d<<24|d>>>8)&4278255360;return a},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});b.MD5=q._createHelper(d);b.HmacMD5=q._createHmacHelper(d)})(Math); +(function(){var s=CryptoJS,l=s.lib,e=l.Base,n=l.WordArray,l=s.algo,p=l.EvpKDF=e.extend({cfg:e.extend({keySize:4,hasher:l.MD5,iterations:1}),init:function(b){this.cfg=this.cfg.extend(b)},compute:function(b,d){for(var e=this.cfg,q=e.hasher.create(),a=n.create(),l=a.words,p=e.keySize,e=e.iterations;l.length>>2]&255}};e.BlockCipher=q.extend({cfg:q.cfg.extend({mode:a,padding:u}),reset:function(){q.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, +this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var w=e.CipherParams=n.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),a=(l.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?p.create([1398893684, +1701076831]).concat(a).concat(b):b).toString(d)},parse:function(a){a=d.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=p.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return w.create({ciphertext:a,salt:c})}},c=e.SerializableCipher=n.extend({cfg:n.extend({format:a}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var e=a.createEncryptor(c,d);b=e.finalize(b);e=e.cfg;return w.create({ciphertext:b,key:c,iv:e.iv,algorithm:a,mode:e.mode,padding:e.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),l=(l.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=p.random(8));a=t.create({keySize:b+c}).compute(a,d);c=p.create(a.words.slice(b),4*c);a.sigBytes=4*b;return w.create({key:a,iv:c,salt:d})}},G=e.PasswordBasedCipher=c.extend({cfg:c.cfg.extend({kdf:l}),encrypt:function(a,b,d,e){e=this.cfg.extend(e);d=e.kdf.execute(d, +a.keySize,a.ivSize);e.iv=d.iv;a=c.encrypt.call(this,a,b,d.key,e);a.mixIn(d);return a},decrypt:function(a,b,d,e){e=this.cfg.extend(e);b=this._parse(b,e.format);d=e.kdf.execute(d,a.keySize,a.ivSize,b.salt);e.iv=d.iv;return c.decrypt.call(this,a,b,d.key,e)}})}(); +(function(){function s(){for(var b=this._S,d=this._i,e=this._j,q=0,a=0;4>a;a++){var d=(d+1)%256,e=(e+b[d])%256,l=b[d];b[d]=b[e];b[e]=l;q|=b[(b[d]+b[e])%256]<<24-8*a}this._i=d;this._j=e;return q}var l=CryptoJS,e=l.lib.StreamCipher,n=l.algo,p=n.RC4=e.extend({_doReset:function(){for(var b=this._key,d=b.words,b=b.sigBytes,e=this._S=[],l=0;256>l;l++)e[l]=l;for(var a=l=0;256>l;l++){var n=l%b,a=(a+e[l]+(d[n>>>2]>>>24-8*(n%4)&255))%256,n=e[l];e[l]=e[a];e[a]=n}this._i=this._j=0},_doProcessBlock:function(b, +d){b[d]^=s.call(this)},keySize:8,ivSize:0});l.RC4=e._createHelper(p);n=n.RC4Drop=p.extend({cfg:p.cfg.extend({drop:192}),_doReset:function(){p._doReset.call(this);for(var b=this.cfg.drop;0>>2]|=(h[b>>>2]>>>24-8*(b%4)&255)<<24-8*((d+b)%4);else if(65535>>2]=h[b>>>2];else c.push.apply(c,h);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=j.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(d%4)&255;b.push((g>>>4).toString(16));b.push((g&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],d=0;d>>3]|=parseInt(a.substr(d, +2),16)<<24-4*(d%8);return new u.init(b,c/2)}},A=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],d=0;d>>2]>>>24-8*(d%4)&255));return b.join("")},parse:function(a){for(var b=a.length,h=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return new u.init(h,b)}},g=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(A.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return A.parse(unescape(encodeURIComponent(a)))}}, +v=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new u.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=g.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,h=b.words,d=b.sigBytes,g=this.blockSize,v=d/(4*g),v=a?j.ceil(v):j.max((v|0)-this._minBufferSize,0);a=v*g;d=j.min(4*a,d);if(a){for(var e=0;eb;b++){var a=e+b,c=g[a];g[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360}var a=this._hash.words,c=D.words,h=A.words,d=z.words,j=t.words,k=u.words,l=w.words,B,m,n,p,x,C,q,r,s,y;C=B=a[0];q=m=a[1];r=n=a[2];s=p=a[3];y=x=a[4];for(var f,b=0;80>b;b+=1)f=B+g[e+d[b]]|0,f=16>b?f+((m^n^p)+c[0]):32>b?f+((m&n|~m&p)+c[1]):48>b? +f+(((m|~n)^p)+c[2]):64>b?f+((m&p|n&~p)+c[3]):f+((m^(n|~p))+c[4]),f|=0,f=f<>>32-k[b],f=f+x|0,B=x,x=p,p=n<<10|n>>>22,n=m,m=f,f=C+g[e+j[b]]|0,f=16>b?f+((q^(r|~s))+h[0]):32>b?f+((q&s|r&~s)+h[1]):48>b?f+(((q|~r)^s)+h[2]):64>b?f+((q&r|~q&s)+h[3]):f+((q^r^s)+h[4]),f|=0,f=f<>>32-l[b],f=f+y|0,C=y,y=s,s=r<<10|r>>>22,r=q,q=f;f=a[1]+n+s|0;a[1]=a[2]+p+y|0;a[2]=a[3]+x+C|0;a[3]=a[4]+B+q|0;a[4]=a[0]+m+r|0;a[0]=f},_doFinalize:function(){var g=this._data,e=g.words,b=8*this._nDataBytes,a=8*g.sigBytes; +e[a>>>5]|=128<<24-a%32;e[(a+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;g.sigBytes=4*(e.length+1);this._process();g=this._hash;e=g.words;for(b=0;5>b;b++)a=e[b],e[b]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;return g},clone:function(){var e=l.clone.call(this);e._hash=this._hash.clone();return e}});j.RIPEMD160=l._createHelper(k);j.HmacRIPEMD160=l._createHmacHelper(k)})(Math); diff --git a/app/src/main/assets/js/cryptojs/sha1.js b/app/src/main/assets/js/cryptojs/sha1.js new file mode 100644 index 000000000..ef0f36e94 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha1.js @@ -0,0 +1,15 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(e,m){var p={},j=p.lib={},l=function(){},f=j.Base={extend:function(a){l.prototype=this;var c=new l;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +n=j.WordArray=f.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=m?c:4*a.length},toString:function(a){return(a||h).stringify(this)},concat:function(a){var c=this.words,q=a.words,d=this.sigBytes;a=a.sigBytes;this.clamp();if(d%4)for(var b=0;b>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((d+b)%4);else if(65535>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=e.ceil(c/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(d%4)&255;b.push((f>>>4).toString(16));b.push((f&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],d=0;d>>3]|=parseInt(a.substr(d, +2),16)<<24-4*(d%8);return new n.init(b,c/2)}},g=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],d=0;d>>2]>>>24-8*(d%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return new n.init(b,c)}},r=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(g.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return g.parse(unescape(encodeURIComponent(a)))}}, +k=j.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new n.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=r.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,d=c.sigBytes,f=this.blockSize,h=d/(4*f),h=a?e.ceil(h):e.max((h|0)-this._minBufferSize,0);a=h*f;d=e.min(4*a,d);if(a){for(var g=0;ga;a++){if(16>a)l[a]=f[n+a]|0;else{var c=l[a-3]^l[a-8]^l[a-14]^l[a-16];l[a]=c<<1|c>>>31}c=(h<<5|h>>>27)+j+l[a];c=20>a?c+((g&e|~g&k)+1518500249):40>a?c+((g^e^k)+1859775393):60>a?c+((g&e|g&k|e&k)-1894007588):c+((g^e^ +k)-899497514);j=k;k=e;e=g<<30|g>>>2;g=h;h=c}b[0]=b[0]+h|0;b[1]=b[1]+g|0;b[2]=b[2]+e|0;b[3]=b[3]+k|0;b[4]=b[4]+j|0},_doFinalize:function(){var f=this._data,e=f.words,b=8*this._nDataBytes,h=8*f.sigBytes;e[h>>>5]|=128<<24-h%32;e[(h+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(h+64>>>9<<4)+15]=b;f.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=j.clone.call(this);e._hash=this._hash.clone();return e}});e.SHA1=j._createHelper(m);e.HmacSHA1=j._createHmacHelper(m)})(); diff --git a/app/src/main/assets/js/cryptojs/sha224.js b/app/src/main/assets/js/cryptojs/sha224.js new file mode 100644 index 000000000..897728620 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha224.js @@ -0,0 +1,17 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(g,l){var f={},k=f.lib={},h=function(){},m=k.Base={extend:function(a){h.prototype=this;var c=new h;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=k.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=l?c:4*a.length},toString:function(a){return(a||s).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new q.init(d,c/2)}},n=t.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},j=t.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, +w=k.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?g.ceil(f):g.max((f|0)-this._minBufferSize,0);a=f*e;b=g.min(4*a,b);if(a){for(var u=0;un;){var j;a:{j=s;for(var w=g.sqrt(j),v=2;v<=w;v++)if(!(j%v)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=t(g.pow(s,0.5))),q[n]=t(g.pow(s,1/3)),n++);s++}var a=[],f=f.SHA256=h.extend({_doReset:function(){this._hash=new k.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],k=b[3],h=b[4],l=b[5],m=b[6],n=b[7],p=0;64>p;p++){if(16>p)a[p]= +c[d+p]|0;else{var j=a[p-15],r=a[p-2];a[p]=((j<<25|j>>>7)^(j<<14|j>>>18)^j>>>3)+a[p-7]+((r<<15|r>>>17)^(r<<13|r>>>19)^r>>>10)+a[p-16]}j=n+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&l^~h&m)+q[p]+a[p];r=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);n=m;m=l;l=h;h=k+j|0;k=g;g=f;f=e;e=j+r|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+k|0;b[4]=b[4]+h|0;b[5]=b[5]+l|0;b[6]=b[6]+m|0;b[7]=b[7]+n|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=g.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=h.clone.call(this);a._hash=this._hash.clone();return a}});l.SHA256=h._createHelper(f);l.HmacSHA256=h._createHmacHelper(f)})(Math); +(function(){var g=CryptoJS,l=g.lib.WordArray,f=g.algo,k=f.SHA256,f=f.SHA224=k.extend({_doReset:function(){this._hash=new l.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var f=k._doFinalize.call(this);f.sigBytes-=4;return f}});g.SHA224=k._createHelper(f);g.HmacSHA224=k._createHmacHelper(f)})(); diff --git a/app/src/main/assets/js/cryptojs/sha256.js b/app/src/main/assets/js/cryptojs/sha256.js new file mode 100644 index 000000000..65b657c1a --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha256.js @@ -0,0 +1,16 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, +x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;mk;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]= +c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math); diff --git a/app/src/main/assets/js/cryptojs/sha3.js b/app/src/main/assets/js/cryptojs/sha3.js new file mode 100644 index 000000000..76ab247d3 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha3.js @@ -0,0 +1,19 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(v,p){var d={},u=d.lib={},r=function(){},f=u.Base={extend:function(a){r.prototype=this;var b=new r;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +s=u.WordArray=f.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=p?b:4*a.length},toString:function(a){return(a||y).stringify(this)},concat:function(a){var b=this.words,c=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var n=0;n>>2]|=(c[n>>>2]>>>24-8*(n%4)&255)<<24-8*((j+n)%4);else if(65535>>2]=c[n>>>2];else b.push.apply(b,c);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<< +32-8*(b%4);a.length=v.ceil(b/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],c=0;c>>2]>>>24-8*(j%4)&255;c.push((n>>>4).toString(16));c.push((n&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],j=0;j>>3]|=parseInt(a.substr(j, +2),16)<<24-4*(j%8);return new s.init(c,b/2)}},e=x.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var c=[],j=0;j>>2]>>>24-8*(j%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new s.init(c,b)}},q=x.Utf8={stringify:function(a){try{return decodeURIComponent(escape(e.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return e.parse(unescape(encodeURIComponent(a)))}}, +t=u.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new s.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=q.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,c=b.words,j=b.sigBytes,n=this.blockSize,e=j/(4*n),e=a?v.ceil(e):v.max((e|0)-this._minBufferSize,0);a=e*n;j=v.min(4*a,j);if(a){for(var f=0;ft;t++){s[e+5*q]=(t+1)*(t+2)/2%64;var w=(2*e+3*q)%5,e=q%5,q=w}for(e=0;5>e;e++)for(q=0;5>q;q++)x[e+5*q]=q+5*((2*e+3*q)%5);e=1;for(q=0;24>q;q++){for(var a=w=t=0;7>a;a++){if(e&1){var b=(1<b?w^=1<e;e++)c[e]=f.create();d=d.SHA3=r.extend({cfg:r.cfg.extend({outputLength:512}),_doReset:function(){for(var a=this._state= +[],b=0;25>b;b++)a[b]=new f.init;this.blockSize=(1600-2*this.cfg.outputLength)/32},_doProcessBlock:function(a,b){for(var e=this._state,f=this.blockSize/2,h=0;h>>24)&16711935|(l<<24|l>>>8)&4278255360,m=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360,g=e[h];g.high^=m;g.low^=l}for(f=0;24>f;f++){for(h=0;5>h;h++){for(var d=l=0,k=0;5>k;k++)g=e[h+5*k],l^=g.high,d^=g.low;g=c[h];g.high=l;g.low=d}for(h=0;5>h;h++){g=c[(h+4)%5];l=c[(h+1)%5];m=l.high;k=l.low;l=g.high^ +(m<<1|k>>>31);d=g.low^(k<<1|m>>>31);for(k=0;5>k;k++)g=e[h+5*k],g.high^=l,g.low^=d}for(m=1;25>m;m++)g=e[m],h=g.high,g=g.low,k=s[m],32>k?(l=h<>>32-k,d=g<>>32-k):(l=g<>>64-k,d=h<>>64-k),g=c[x[m]],g.high=l,g.low=d;g=c[0];h=e[0];g.high=h.high;g.low=h.low;for(h=0;5>h;h++)for(k=0;5>k;k++)m=h+5*k,g=e[m],l=c[m],m=c[(h+1)%5+5*k],d=c[(h+2)%5+5*k],g.high=l.high^~m.high&d.high,g.low=l.low^~m.low&d.low;g=e[0];h=y[f];g.high^=h.high;g.low^=h.low}},_doFinalize:function(){var a=this._data, +b=a.words,c=8*a.sigBytes,e=32*this.blockSize;b[c>>>5]|=1<<24-c%32;b[(v.ceil((c+1)/e)*e>>>5)-1]|=128;a.sigBytes=4*b.length;this._process();for(var a=this._state,b=this.cfg.outputLength/8,c=b/8,e=[],h=0;h>>24)&16711935|(f<<24|f>>>8)&4278255360,d=(d<<8|d>>>24)&16711935|(d<<24|d>>>8)&4278255360;e.push(d);e.push(f)}return new u.init(e,b)},clone:function(){for(var a=r.clone.call(this),b=a._state=this._state.slice(0),c=0;25>c;c++)b[c]=b[c].clone();return a}}); +p.SHA3=r._createHelper(d);p.HmacSHA3=r._createHmacHelper(d)})(Math); diff --git a/app/src/main/assets/js/cryptojs/sha384.js b/app/src/main/assets/js/cryptojs/sha384.js new file mode 100644 index 000000000..f01a710a2 --- /dev/null +++ b/app/src/main/assets/js/cryptojs/sha384.js @@ -0,0 +1,25 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +CryptoJS=CryptoJS||function(a,c){var d={},j=d.lib={},f=function(){},m=j.Base={extend:function(a){f.prototype=this;var b=new f;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +B=j.WordArray=m.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=c?b:4*a.length},toString:function(a){return(a||y).stringify(this)},concat:function(a){var b=this.words,g=a.words,e=this.sigBytes;a=a.sigBytes;this.clamp();if(e%4)for(var k=0;k>>2]|=(g[k>>>2]>>>24-8*(k%4)&255)<<24-8*((e+k)%4);else if(65535>>2]=g[k>>>2];else b.push.apply(b,g);this.sigBytes+=a;return this},clamp:function(){var n=this.words,b=this.sigBytes;n[b>>>2]&=4294967295<< +32-8*(b%4);n.length=a.ceil(b/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(n){for(var b=[],g=0;g>>2]>>>24-8*(e%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(a){for(var b=a.length,g=[],e=0;e>>3]|=parseInt(a.substr(e, +2),16)<<24-4*(e%8);return new B.init(g,b/2)}},F=v.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var g=[],e=0;e>>2]>>>24-8*(e%4)&255));return g.join("")},parse:function(a){for(var b=a.length,g=[],e=0;e>>2]|=(a.charCodeAt(e)&255)<<24-8*(e%4);return new B.init(g,b)}},ha=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(F.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return F.parse(unescape(encodeURIComponent(a)))}}, +Z=j.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new B.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=ha.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(n){var b=this._data,g=b.words,e=b.sigBytes,k=this.blockSize,m=e/(4*k),m=n?a.ceil(m):a.max((m|0)-this._minBufferSize,0);n=m*k;e=a.min(4*n,e);if(n){for(var c=0;cy;y++)v[y]=a();j=j.SHA512=d.extend({_doReset:function(){this._hash=new m.init([new f.init(1779033703,4089235720),new f.init(3144134277,2227873595),new f.init(1013904242,4271175723),new f.init(2773480762,1595750129),new f.init(1359893119,2917565137),new f.init(2600822924,725511199),new f.init(528734635,4215389547),new f.init(1541459225,327033209)])},_doProcessBlock:function(a,c){for(var d=this._hash.words, +f=d[0],j=d[1],b=d[2],g=d[3],e=d[4],k=d[5],m=d[6],d=d[7],y=f.high,M=f.low,$=j.high,N=j.low,aa=b.high,O=b.low,ba=g.high,P=g.low,ca=e.high,Q=e.low,da=k.high,R=k.low,ea=m.high,S=m.low,fa=d.high,T=d.low,s=y,p=M,G=$,D=N,H=aa,E=O,W=ba,I=P,t=ca,q=Q,U=da,J=R,V=ea,K=S,X=fa,L=T,u=0;80>u;u++){var z=v[u];if(16>u)var r=z.high=a[c+2*u]|0,h=z.low=a[c+2*u+1]|0;else{var r=v[u-15],h=r.high,w=r.low,r=(h>>>1|w<<31)^(h>>>8|w<<24)^h>>>7,w=(w>>>1|h<<31)^(w>>>8|h<<24)^(w>>>7|h<<25),C=v[u-2],h=C.high,l=C.low,C=(h>>>19|l<< +13)^(h<<3|l>>>29)^h>>>6,l=(l>>>19|h<<13)^(l<<3|h>>>29)^(l>>>6|h<<26),h=v[u-7],Y=h.high,A=v[u-16],x=A.high,A=A.low,h=w+h.low,r=r+Y+(h>>>0>>0?1:0),h=h+l,r=r+C+(h>>>0>>0?1:0),h=h+A,r=r+x+(h>>>0>>0?1:0);z.high=r;z.low=h}var Y=t&U^~t&V,A=q&J^~q&K,z=s&G^s&H^G&H,ja=p&D^p&E^D&E,w=(s>>>28|p<<4)^(s<<30|p>>>2)^(s<<25|p>>>7),C=(p>>>28|s<<4)^(p<<30|s>>>2)^(p<<25|s>>>7),l=B[u],ka=l.high,ga=l.low,l=L+((q>>>14|t<<18)^(q>>>18|t<<14)^(q<<23|t>>>9)),x=X+((t>>>14|q<<18)^(t>>>18|q<<14)^(t<<23|q>>>9))+(l>>>0< +L>>>0?1:0),l=l+A,x=x+Y+(l>>>0>>0?1:0),l=l+ga,x=x+ka+(l>>>0>>0?1:0),l=l+h,x=x+r+(l>>>0>>0?1:0),h=C+ja,z=w+z+(h>>>0>>0?1:0),X=V,L=K,V=U,K=J,U=t,J=q,q=I+l|0,t=W+x+(q>>>0>>0?1:0)|0,W=H,I=E,H=G,E=D,G=s,D=p,p=l+h|0,s=x+z+(p>>>0>>0?1:0)|0}M=f.low=M+p;f.high=y+s+(M>>>0