├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── kotlinc.xml └── misc.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ ├── org.apache.http.legacy.jar │ ├── xwalk_core_library-23.53.589.4.aar │ └── xwalk_shared_library-23.53.589.4.aar ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── cineplex.ttf │ │ └── xwalk-command-line │ ├── java │ │ ├── com │ │ │ └── btr │ │ │ │ └── proxy │ │ │ │ └── selector │ │ │ │ └── pac │ │ │ │ ├── PacProxySelector.java │ │ │ │ ├── PacScriptMethods.java │ │ │ │ ├── PacScriptParser.java │ │ │ │ ├── PacScriptSource.java │ │ │ │ ├── Proxy.java │ │ │ │ ├── ProxyEvaluationException.java │ │ │ │ ├── ProxyException.java │ │ │ │ ├── RhinoPacScriptParser.java │ │ │ │ ├── ScriptMethods.java │ │ │ │ └── UrlPacScriptSource.java │ │ ├── net │ │ │ └── gotev │ │ │ │ └── speech │ │ │ │ ├── DefaultLoggerDelegate.java │ │ │ │ ├── DelayedOperation.java │ │ │ │ ├── GoogleVoiceTypingDisabledException.java │ │ │ │ ├── Logger.java │ │ │ │ ├── Speech.java │ │ │ │ ├── SpeechDelegate.java │ │ │ │ ├── SpeechRecognitionException.java │ │ │ │ ├── SpeechRecognitionNotAvailable.java │ │ │ │ ├── SpeechUtil.java │ │ │ │ ├── SupportedLanguagesListener.java │ │ │ │ ├── TextToSpeechCallback.java │ │ │ │ ├── TtsProgressListener.java │ │ │ │ ├── UnsupportedReason.java │ │ │ │ ├── engine │ │ │ │ ├── BaseSpeechRecognitionEngine.java │ │ │ │ ├── BaseTextToSpeechEngine.java │ │ │ │ ├── DummyOnInitListener.java │ │ │ │ ├── SpeechRecognitionEngine.java │ │ │ │ └── TextToSpeechEngine.java │ │ │ │ └── ui │ │ │ │ ├── SpeechBar.java │ │ │ │ ├── SpeechProgressView.java │ │ │ │ └── animators │ │ │ │ ├── BarParamsAnimator.java │ │ │ │ ├── BarRmsAnimator.java │ │ │ │ ├── IdleAnimator.java │ │ │ │ ├── RmsAnimator.java │ │ │ │ ├── RotatingAnimator.java │ │ │ │ └── TransformAnimator.java │ │ ├── org │ │ │ └── xwalk │ │ │ │ └── core │ │ │ │ ├── MyXWalkDialogManager.java │ │ │ │ ├── MyXWalkEnvironment.java │ │ │ │ ├── MyXWalkLibraryLoader.java │ │ │ │ └── MyXWalkUpdater.java │ │ └── top │ │ │ └── rootu │ │ │ └── lampa │ │ │ ├── AndroidJS.kt │ │ │ ├── App.kt │ │ │ ├── AppListAdapter.kt │ │ │ ├── AsyncTask.kt │ │ │ ├── AutoCompleteTV.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── CrashActivity.kt │ │ │ ├── ImgArrayAdapter.kt │ │ │ ├── LampaGlideModule.kt │ │ │ ├── LoadingUtils.kt │ │ │ ├── LottieLoader.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PlayerStateManager.kt │ │ │ ├── UpdateActivity.kt │ │ │ ├── browser │ │ │ ├── Browser.kt │ │ │ ├── SysView.kt │ │ │ └── XWalk.kt │ │ │ ├── channels │ │ │ ├── ChannelManager.kt │ │ │ ├── LampaChannels.kt │ │ │ └── WatchNext.kt │ │ │ ├── content │ │ │ ├── Bookmarks.kt │ │ │ ├── Continued.kt │ │ │ ├── History.kt │ │ │ ├── LampaProvider.kt │ │ │ ├── Like.kt │ │ │ ├── Look.kt │ │ │ ├── Recs.kt │ │ │ ├── Scheduled.kt │ │ │ ├── Thrown.kt │ │ │ └── Viewed.kt │ │ │ ├── helpers │ │ │ ├── AppVersion.kt │ │ │ ├── Backup.kt │ │ │ ├── ChannelHelper.kt │ │ │ ├── Coroutines.kt │ │ │ ├── Extensions.kt │ │ │ ├── FileHelpers.java │ │ │ ├── Helpers.kt │ │ │ ├── PermHelpers.kt │ │ │ ├── Prefs.kt │ │ │ └── Updater.kt │ │ │ ├── models │ │ │ ├── Lampa.kt │ │ │ └── Releases.kt │ │ │ ├── net │ │ │ ├── Http.java │ │ │ ├── HttpHelper.java │ │ │ ├── IgnoreSSLTrustManager.java │ │ │ ├── SelfSignedTrustManager.java │ │ │ ├── TlsSniSocketFactory.java │ │ │ └── TlsSocketFactory.java │ │ │ ├── receivers │ │ │ ├── BootReceiver.kt │ │ │ └── HomeWatch.kt │ │ │ ├── recs │ │ │ └── RecsService.kt │ │ │ ├── sched │ │ │ ├── ContentAlarmManager.kt │ │ │ ├── ContentJobService.kt │ │ │ └── Scheduler.kt │ │ │ ├── search │ │ │ ├── SearchCommand.kt │ │ │ ├── SearchDatabase.kt │ │ │ ├── SearchLocal.kt │ │ │ ├── SearchProvider.kt │ │ │ └── Utils.kt │ │ │ └── tmdb │ │ │ ├── Images.kt │ │ │ ├── TMDB.kt │ │ │ └── models │ │ │ ├── content_ratings │ │ │ ├── ContentRatingsModel.kt │ │ │ └── Result.kt │ │ │ ├── entity │ │ │ ├── BelongsToCollection.kt │ │ │ ├── Entities.kt │ │ │ ├── Entity.kt │ │ │ ├── Genre.kt │ │ │ ├── Image.kt │ │ │ ├── Images.kt │ │ │ ├── ProductionCompany.kt │ │ │ ├── ProductionCountry.kt │ │ │ ├── Season.kt │ │ │ └── SpokenLanguage.kt │ │ │ ├── release_dates │ │ │ ├── ReleaseDate.kt │ │ │ ├── ReleaseDatesModel.kt │ │ │ └── Result.kt │ │ │ ├── titles │ │ │ ├── AlternativeTitles.kt │ │ │ └── Result.kt │ │ │ └── trailers │ │ │ ├── Trailer.kt │ │ │ └── Trailers.kt │ └── res │ │ ├── color │ │ └── action_mic.xml │ │ ├── drawable-xhdpi │ │ ├── empty_poster.png │ │ ├── lampa_banner.png │ │ ├── lampa_icon.png │ │ ├── lampa_logo.xml │ │ └── lampa_logo_round.png │ │ ├── drawable-xxxhdpi │ │ ├── lampa_banner.png │ │ └── lampa_icon.png │ │ ├── drawable │ │ ├── active_menu_bg.xml │ │ ├── ch_book_shape.xml │ │ ├── ch_hist_shape.xml │ │ ├── ch_like_shape.xml │ │ ├── dialog_background.xml │ │ ├── dropdown_bg_focused.xml │ │ ├── dropdown_bg_pressed.xml │ │ ├── dropdown_item_background.xml │ │ ├── dropdown_selector.xml │ │ ├── ic_mic.xml │ │ ├── lampa_banner.png │ │ ├── lampa_logo_icon.xml │ │ ├── round_close_24.xml │ │ ├── round_content_copy_24.xml │ │ ├── round_exit_to_app_24.xml │ │ ├── round_explorer_24.xml │ │ ├── round_link_24.xml │ │ ├── round_refresh_24.xml │ │ ├── round_save_24.xml │ │ ├── round_settings_backup_restore_24.xml │ │ ├── search_orb_selector.xml │ │ └── welcome.xml │ │ ├── layout-land │ │ └── activity_update.xml │ │ ├── layout │ │ ├── activity_crash.xml │ │ ├── activity_empty.xml │ │ ├── activity_update.xml │ │ ├── activity_webview.xml │ │ ├── activity_xwalk.xml │ │ ├── app_list_item.xml │ │ ├── app_list_title.xml │ │ ├── dialog_input_url.xml │ │ ├── dialog_search.xml │ │ ├── error_log_sheet.xml │ │ ├── lampa_dropdown_item.xml │ │ └── loader.xml │ │ ├── raw │ │ ├── cub_watch │ │ ├── cub_watch2 │ │ ├── cub_watch3 │ │ ├── loader.json │ │ └── warn_lottie.json │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── network_security_config.xml │ │ ├── provider_paths.xml │ │ └── searchable.xml │ └── ruStore │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lampa_banner_xxxhdpi.psd └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | - '!*-RuStore' 8 | 9 | permissions: 10 | contents: write 11 | 12 | env: 13 | KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} 14 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 15 | RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }} 16 | RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} 17 | 18 | jobs: 19 | build: 20 | name: Build APK 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - name: set up JDK 17 28 | uses: actions/setup-java@v3 29 | with: 30 | java-version: '17' 31 | distribution: 'adopt' 32 | cache: gradle 33 | - name: Checkout keystore repo 34 | uses: actions/checkout@v3 35 | with: 36 | repository: ${{ secrets.KEYSTORE_GIT_REPOSITORY }} 37 | token: ${{ secrets.KEYSTORE_ACCESS_TOKEN }} 38 | path: app/keystore 39 | - name: Grant execute permission for gradlew 40 | run: chmod +x gradlew 41 | - name: Build with Gradle 42 | run: | 43 | ./gradlew assembleRelease --stacktrace 44 | ./gradlew bundleRelease 45 | - name: Release to RuStore 46 | env: 47 | KEY_ID: ${{ secrets.RUSTORE_KEY_ID }} 48 | PRIVATE_KEY_BASE64: ${{ secrets.RUSTORE_KEY }} 49 | run: | 50 | APP_PACKAGE_NAME="top.rootu.lampa" # пакет приложения 51 | APK_PATH="./app/build/outputs/apk/ruStore/release/app-ruStore-release.apk" # Путь к APK 52 | API_VERSION="https://public-api.rustore.ru/public/v1/application/${APP_PACKAGE_NAME}/version" 53 | # Извлекаем приватный ключ из секрета и декодируем 54 | echo $PRIVATE_KEY_BASE64 | base64 --decode > private_key.pem 55 | # Генерация timestamp 56 | TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ") 57 | # Создаем сообщение для подписи 58 | MESSAGE="${KEY_ID}${TIMESTAMP}" 59 | # Генерация подписи с помощью OpenSSL 60 | SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha512 -sign private_key.pem | base64 -w 0) 61 | echo Get TOKEN 62 | # Получение JWT токена 63 | JSON_DATA="{\"keyId\":\"$KEY_ID\",\"timestamp\":\"$TIMESTAMP\",\"signature\":\"$SIGNATURE\"}" 64 | RESPONSE=$(curl --location --request POST 'https://public-api.rustore.ru/public/auth' \ 65 | --header 'Content-Type: application/json' \ 66 | --data "$JSON_DATA") 67 | # Проверка кода ответа 68 | CODE=$(echo $RESPONSE | jq -r '.code') 69 | if [ "$CODE" != "OK" ]; then 70 | echo $RESPONSE 71 | echo "Error occurred: $(echo $RESPONSE | jq -r '.message')" 72 | exit 1 # Завершение сборки с ошибкой 73 | fi 74 | echo OK 75 | # Извлекаем jwe из ответа 76 | JWT_TOKEN=$(echo $RESPONSE | jq -r '.body.jwe') 77 | # Создаём черновик 78 | echo Create release 79 | RESPONSE=$(curl --location --request POST "${API_VERSION}" \ 80 | --header "Public-Token: ${JWT_TOKEN}" \ 81 | --header 'Content-Type: application/json' \ 82 | --data "{}") 83 | echo $RESPONSE 84 | # Проверка кода ответа 85 | CODE=$(echo $RESPONSE | jq -r '.code') 86 | if [ "$CODE" != "OK" ]; then 87 | echo "Error occurred: $(echo $RESPONSE | jq -r '.message')" 88 | exit 1 # Завершение сборки с ошибкой 89 | fi 90 | VERSION_CODE=$(echo $RESPONSE | jq -r '.body') 91 | echo OK 92 | # Публикуем apk 93 | echo "Public APK $VERSION_CODE" 94 | RESPONSE=$(curl --location --request POST "${API_VERSION}/${VERSION_CODE}/apk?servicesType=Unknown&isMainApk=true" \ 95 | --header "Public-Token: ${JWT_TOKEN}" \ 96 | --form "file=@\"${APK_PATH}\"") 97 | echo $RESPONSE 98 | # Проверка кода ответа 99 | CODE=$(echo $RESPONSE | jq -r '.code') 100 | if [ "$CODE" != "OK" ]; then 101 | echo "Error occurred: $(echo $RESPONSE | jq -r '.message')" 102 | echo Delete bad release 103 | RESPONSE=$(curl --location --request DELETE "${API_VERSION}/${VERSION_CODE}" \ 104 | --header "Public-Token: ${JWT_TOKEN}" \ 105 | --header 'Content-Type: application/json' \ 106 | --data "{}") 107 | echo $RESPONSE 108 | exit 1 # Завершение сборки с ошибкой 109 | fi 110 | echo OK 111 | # Коммитим 112 | echo Commit release 113 | RESPONSE=$(curl --location --request POST "${API_VERSION}/${VERSION_CODE}/commit" \ 114 | --header "Public-Token: ${JWT_TOKEN}") 115 | echo $RESPONSE 116 | # Проверка кода ответа 117 | CODE=$(echo $RESPONSE | jq -r '.code') 118 | if [ "$CODE" != "OK" ]; then 119 | echo "Error occurred: $(echo $RESPONSE | jq -r '.message')" 120 | exit 1 # Завершение сборки с ошибкой 121 | fi 122 | echo OK 123 | - name: Release 124 | uses: softprops/action-gh-release@v1 125 | if: startsWith(github.ref, 'refs/tags/') 126 | with: 127 | files: app/build/outputs/apk/lite/release/app-lite-release.apk 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Mac stuff 88 | .DS_Store 89 | /app/keystore/ 90 | .idea/ 91 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 |

4 |
5 |

6 | LAMPA client browser for Android and Android TV 7 |
8 |
9 | 10 | CodeFactor 11 | 12 | 13 | Build 14 | 15 | 16 | GitHub tag (latest SemVer pre-release) 17 | 18 |

19 |

20 | System requirements: Android 4.1+ (API level 16+) 21 |

22 | 23 | ### Last release links: 24 | - [Release page](https://github.com/lampa-app/LAMPA/releases/latest) 25 | - [Direct apk download link](https://github.com/lampa-app/LAMPA/releases/latest/download/app-lite-release.apk) 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/libs/org.apache.http.legacy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/libs/org.apache.http.legacy.jar -------------------------------------------------------------------------------- /app/libs/xwalk_core_library-23.53.589.4.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/libs/xwalk_core_library-23.53.589.4.aar -------------------------------------------------------------------------------- /app/libs/xwalk_shared_library-23.53.589.4.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/libs/xwalk_shared_library-23.53.589.4.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keepclassmembers class top.rootu.lampa.AndroidJS { 23 | public *; 24 | } 25 | -keep class kotlin.** { *; } 26 | -keep class kotlin.collections.** { *; } 27 | -keep class kotlin.ranges.** { *; } 28 | -keep class kotlin.sequences.** { *; } 29 | -keep class kotlin.text.** { *; } 30 | -keep class org.xwalk.core.** { *; } 31 | -dontwarn org.xwalk.core.** 32 | -keep class j$.util.** { *; } 33 | -dontwarn j$.util.** 34 | -------------------------------------------------------------------------------- /app/src/main/assets/cineplex.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/assets/cineplex.ttf -------------------------------------------------------------------------------- /app/src/main/assets/xwalk-command-line: -------------------------------------------------------------------------------- 1 | xwalk --enable-experimental-canvas-features --disable-pull-to-refresh-effect --ignore-gpu-blacklist --disk-cache-size=1 --disable-web-security --enable-wasm -------------------------------------------------------------------------------- /app/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java: -------------------------------------------------------------------------------- 1 | package com.btr.proxy.selector.pac; 2 | 3 | /*************************************************************************** 4 | * Common interface for PAC script parsers. 5 | * 6 | * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 7 | ***************************************************************************/ 8 | public interface PacScriptParser { 9 | 10 | /*************************************************************************** 11 | * Gets the source of the PAC script used by this parser. 12 | * 13 | * @return a PacScriptSource. 14 | **************************************************************************/ 15 | PacScriptSource getScriptSource(); 16 | 17 | /************************************************************************* 18 | * Evaluates the given URL and host against the PAC script. 19 | * 20 | * @param url 21 | * the URL to evaluate. 22 | * @param host 23 | * the host name part of the URL. 24 | * @return the script result. 25 | * @throws ProxyEvaluationException 26 | * on execution error. 27 | ************************************************************************/ 28 | String evaluate(String url, String host) 29 | throws ProxyEvaluationException; 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java: -------------------------------------------------------------------------------- 1 | package com.btr.proxy.selector.pac; 2 | 3 | import java.io.IOException; 4 | 5 | /***************************************************************************** 6 | * An source to fetch the PAC script from. 7 | * 8 | * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 9 | ****************************************************************************/ 10 | 11 | public interface PacScriptSource { 12 | 13 | /************************************************************************* 14 | * Gets the PAC script content as String. 15 | * 16 | * @return a script. 17 | * @throws IOException 18 | * on read error. 19 | ************************************************************************/ 20 | 21 | String getScriptContent() throws IOException; 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/btr/proxy/selector/pac/Proxy.java: -------------------------------------------------------------------------------- 1 | package com.btr.proxy.selector.pac; 2 | 3 | public class Proxy { 4 | 5 | public final static Proxy NO_PROXY = new Proxy(null, 0, null); 6 | 7 | public final static String TYPE_HTTP = "http"; 8 | public final static String TYPE_HTTPS = "https"; 9 | public final static String TYPE_SOCKS4 = "socks4"; 10 | public final static String TYPE_SOCKS5 = "socks5"; 11 | 12 | public String type = "http"; 13 | public String host = ""; 14 | public int port = 3128; 15 | 16 | public Proxy(String host, int port, String type) { 17 | this.host = host; 18 | this.port = port; 19 | this.type = type; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java: -------------------------------------------------------------------------------- 1 | package com.btr.proxy.selector.pac; 2 | 3 | /***************************************************************************** 4 | * Exception for PAC script errors. 5 | * 6 | * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 7 | ****************************************************************************/ 8 | 9 | public class ProxyEvaluationException extends ProxyException { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | /************************************************************************* 14 | * Constructor 15 | ************************************************************************/ 16 | 17 | public ProxyEvaluationException() { 18 | super(); 19 | } 20 | 21 | /************************************************************************* 22 | * Constructor 23 | * 24 | * @param message 25 | * the error message. 26 | * @param cause 27 | * the causing exception for exception chaining. 28 | ************************************************************************/ 29 | 30 | public ProxyEvaluationException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | 34 | /************************************************************************* 35 | * Constructor 36 | * 37 | * @param message 38 | * the error message. 39 | ************************************************************************/ 40 | 41 | public ProxyEvaluationException(String message) { 42 | super(message); 43 | } 44 | 45 | /************************************************************************* 46 | * Constructor 47 | * 48 | * @param cause 49 | * the causing exception for exception chaining. 50 | ************************************************************************/ 51 | 52 | public ProxyEvaluationException(Throwable cause) { 53 | super(cause); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/btr/proxy/selector/pac/ProxyException.java: -------------------------------------------------------------------------------- 1 | package com.btr.proxy.selector.pac; 2 | 3 | /***************************************************************************** 4 | * Indicates an exception in the proxy framework. 5 | * 6 | * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009 7 | ****************************************************************************/ 8 | 9 | public class ProxyException extends Exception { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | /************************************************************************* 14 | * Constructor 15 | ************************************************************************/ 16 | 17 | public ProxyException() { 18 | super(); 19 | } 20 | 21 | /************************************************************************* 22 | * Constructor 23 | * 24 | * @param message 25 | * the error message 26 | * @param cause 27 | * the causing exception for chaining exceptions. 28 | ************************************************************************/ 29 | 30 | public ProxyException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | 34 | /************************************************************************* 35 | * Constructor 36 | * 37 | * @param message 38 | * the error message 39 | ************************************************************************/ 40 | 41 | public ProxyException(String message) { 42 | super(message); 43 | } 44 | 45 | /************************************************************************* 46 | * Constructor 47 | * 48 | * @param cause 49 | * the causing exception for chaining exceptions. 50 | ************************************************************************/ 51 | 52 | public ProxyException(Throwable cause) { 53 | super(cause); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/DefaultLoggerDelegate.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Default logger delegate implementation which logs in LogCat with {@link Log}. 7 | * Log tag is set to UploadService for all the logs. 8 | * @author gotev (Aleksandar Gotev) 9 | */ 10 | public class DefaultLoggerDelegate implements Logger.LoggerDelegate { 11 | 12 | private static final String TAG = Speech.class.getSimpleName(); 13 | 14 | @Override 15 | public void error(String tag, String message) { 16 | Log.e(TAG, tag + " - " + message); 17 | } 18 | 19 | @Override 20 | public void error(String tag, String message, Throwable exception) { 21 | Log.e(TAG, tag + " - " + message, exception); 22 | } 23 | 24 | @Override 25 | public void debug(String tag, String message) { 26 | Log.d(TAG, tag + " - " + message); 27 | } 28 | 29 | @Override 30 | public void info(String tag, String message) { 31 | Log.i(TAG, tag + " - " + message); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/DelayedOperation.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | 6 | import java.util.Timer; 7 | import java.util.TimerTask; 8 | 9 | /** 10 | * @author Aleksandar Gotev 11 | */ 12 | public class DelayedOperation { 13 | 14 | private static final String LOG_TAG = DelayedOperation.class.getSimpleName(); 15 | 16 | public interface Operation { 17 | void onDelayedOperation(); 18 | boolean shouldExecuteDelayedOperation(); 19 | } 20 | 21 | private long mDelay; 22 | private Operation mOperation; 23 | private Timer mTimer; 24 | private boolean started; 25 | private Context mContext; 26 | private String mTag; 27 | 28 | public DelayedOperation(Context context, String tag, long delayInMilliseconds) { 29 | if (context == null) { 30 | throw new IllegalArgumentException("Context is null"); 31 | } 32 | 33 | if (delayInMilliseconds <= 0) { 34 | throw new IllegalArgumentException("The delay in milliseconds must be > 0"); 35 | } 36 | 37 | mContext = context; 38 | mTag = tag; 39 | mDelay = delayInMilliseconds; 40 | Logger.debug(LOG_TAG, "created delayed operation with tag: " + mTag); 41 | } 42 | 43 | public void start(final Operation operation) { 44 | if (operation == null) { 45 | throw new IllegalArgumentException("The operation must be defined!"); 46 | } 47 | 48 | Logger.debug(LOG_TAG, "starting delayed operation with tag: " + mTag); 49 | mOperation = operation; 50 | cancel(); 51 | started = true; 52 | resetTimer(); 53 | } 54 | 55 | public void resetTimer() { 56 | if (!started) return; 57 | 58 | if (mTimer != null) mTimer.cancel(); 59 | 60 | Logger.debug(LOG_TAG, "resetting delayed operation with tag: " + mTag); 61 | mTimer = new Timer(); 62 | mTimer.schedule(new TimerTask() { 63 | @Override 64 | public void run() { 65 | if (mOperation.shouldExecuteDelayedOperation()) { 66 | Logger.debug(LOG_TAG, "executing delayed operation with tag: " + mTag); 67 | new Handler(mContext.getMainLooper()).post(new Runnable() { 68 | @Override 69 | public void run() { 70 | mOperation.onDelayedOperation(); 71 | } 72 | }); 73 | } 74 | cancel(); 75 | } 76 | }, mDelay); 77 | } 78 | 79 | public void cancel() { 80 | if (mTimer != null) { 81 | Logger.debug(LOG_TAG, "cancelled delayed operation with tag: " + mTag); 82 | mTimer.cancel(); 83 | mTimer = null; 84 | } 85 | 86 | started = false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/GoogleVoiceTypingDisabledException.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | /** 4 | * @author Aleksandar Gotev 5 | */ 6 | 7 | public class GoogleVoiceTypingDisabledException extends Exception { 8 | public GoogleVoiceTypingDisabledException() { 9 | super("Google voice typing must be enabled"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/Logger.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import top.rootu.lampa.BuildConfig; 4 | 5 | /** 6 | * Android Speech library logger. 7 | * You can provide your own logger delegate implementation, to be able to log in a different way. 8 | * By default the log level is set to DEBUG when the build type is debug, and OFF in release. 9 | * The default logger implementation logs in Android's LogCat. 10 | * @author gotev (Aleksandar Gotev) 11 | */ 12 | public class Logger { 13 | 14 | public enum LogLevel { 15 | DEBUG, 16 | INFO, 17 | ERROR, 18 | OFF 19 | } 20 | 21 | public interface LoggerDelegate { 22 | void error(String tag, String message); 23 | void error(String tag, String message, Throwable exception); 24 | void debug(String tag, String message); 25 | void info(String tag, String message); 26 | } 27 | 28 | private LogLevel mLogLevel = BuildConfig.DEBUG ? LogLevel.DEBUG : LogLevel.OFF; 29 | 30 | private LoggerDelegate mDelegate = new DefaultLoggerDelegate(); 31 | 32 | private Logger() { } 33 | 34 | private static class SingletonHolder { 35 | private static final Logger instance = new Logger(); 36 | } 37 | 38 | public static void resetLoggerDelegate() { 39 | synchronized (Logger.class) { 40 | SingletonHolder.instance.mDelegate = new DefaultLoggerDelegate(); 41 | } 42 | } 43 | 44 | public static void setLoggerDelegate(LoggerDelegate delegate) { 45 | if (delegate == null) 46 | throw new IllegalArgumentException("delegate MUST not be null!"); 47 | 48 | synchronized (Logger.class) { 49 | SingletonHolder.instance.mDelegate = delegate; 50 | } 51 | } 52 | 53 | public static void setLogLevel(LogLevel level) { 54 | synchronized (Logger.class) { 55 | SingletonHolder.instance.mLogLevel = level; 56 | } 57 | } 58 | 59 | public static void error(String tag, String message) { 60 | if (SingletonHolder.instance.mLogLevel.compareTo(LogLevel.ERROR) <= 0) { 61 | SingletonHolder.instance.mDelegate.error(tag, message); 62 | } 63 | } 64 | 65 | public static void error(String tag, String message, Throwable exception) { 66 | if (SingletonHolder.instance.mLogLevel.compareTo(LogLevel.ERROR) <= 0) { 67 | SingletonHolder.instance.mDelegate.error(tag, message, exception); 68 | } 69 | } 70 | 71 | public static void info(String tag, String message) { 72 | if (SingletonHolder.instance.mLogLevel.compareTo(LogLevel.INFO) <= 0) { 73 | SingletonHolder.instance.mDelegate.info(tag, message); 74 | } 75 | } 76 | 77 | public static void debug(String tag, String message) { 78 | if (SingletonHolder.instance.mLogLevel.compareTo(LogLevel.DEBUG) <= 0) { 79 | SingletonHolder.instance.mDelegate.debug(tag, message); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/SpeechDelegate.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Speech delegate interface. It contains the methods to receive speech events. 7 | * 8 | * @author Aleksandar Gotev 9 | */ 10 | public interface SpeechDelegate { 11 | 12 | /** 13 | * Invoked when the speech recognition is started. 14 | */ 15 | void onStartOfSpeech(); 16 | 17 | /** 18 | * The sound level in the audio stream has changed. 19 | * There is no guarantee that this method will be called. 20 | * @param value the new RMS dB value 21 | */ 22 | void onSpeechRmsChanged(float value); 23 | 24 | /** 25 | * Invoked when there are partial speech results. 26 | * @param results list of strings. This is ensured to be non null and non empty. 27 | */ 28 | void onSpeechPartialResults(List results); 29 | 30 | /** 31 | * Invoked when there is a speech result 32 | * @param result string resulting from speech recognition. 33 | * This is ensured to be non null. 34 | */ 35 | void onSpeechResult(String result); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/SpeechRecognitionException.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import android.speech.SpeechRecognizer; 4 | 5 | /** 6 | * Speech recognition exception. 7 | * 8 | * @author Aleksandar Gotev 9 | */ 10 | public class SpeechRecognitionException extends Exception { 11 | 12 | private int code; 13 | 14 | public SpeechRecognitionException(int code) { 15 | super(getMessage(code)); 16 | this.code = code; 17 | } 18 | 19 | private static String getMessage(int code) { 20 | String message; 21 | 22 | // these have been mapped from here: 23 | // https://developer.android.com/reference/android/speech/SpeechRecognizer.html 24 | switch(code) { 25 | case SpeechRecognizer.ERROR_AUDIO: 26 | message = code + " - Audio recording error"; 27 | break; 28 | 29 | case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: 30 | message = code + " - Insufficient permissions. Request android.permission.RECORD_AUDIO"; 31 | break; 32 | 33 | case SpeechRecognizer.ERROR_CLIENT: 34 | // http://stackoverflow.com/questions/24995565/android-speechrecognizer-when-do-i-get-error-client-when-starting-the-voice-reco 35 | message = code + " - Client side error. Maybe your internet connection is poor!"; 36 | break; 37 | 38 | case SpeechRecognizer.ERROR_NETWORK: 39 | message = code + " - Network error"; 40 | break; 41 | 42 | case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: 43 | message = code + " - Network operation timed out"; 44 | break; 45 | 46 | case SpeechRecognizer.ERROR_NO_MATCH: 47 | message = code + " - No recognition result matched. Try turning on partial results as a workaround."; 48 | break; 49 | 50 | case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: 51 | message = code + " - RecognitionService busy"; 52 | break; 53 | 54 | case SpeechRecognizer.ERROR_SERVER: 55 | message = code + " - Server sends error status"; 56 | break; 57 | 58 | case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: 59 | message = code + " - No speech input"; 60 | break; 61 | 62 | default: 63 | message = code + " - Unknown exception"; 64 | break; 65 | } 66 | 67 | return message; 68 | } 69 | 70 | public int getCode() { 71 | return code; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/SpeechRecognitionNotAvailable.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | /** 4 | * @author Aleksandar Gotev 5 | */ 6 | public class SpeechRecognitionNotAvailable extends Exception { 7 | public SpeechRecognitionNotAvailable() { 8 | super("Speech recognition not available"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/SpeechUtil.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | /** 8 | * Utility methods. 9 | * 10 | * @author Aleksandar Gotev 11 | */ 12 | public class SpeechUtil { 13 | 14 | // private constructor to avoid instantiation 15 | private SpeechUtil() {} 16 | 17 | /** 18 | * Opens the Google App page on Play Store 19 | * @param context application context 20 | */ 21 | public static void redirectUserToGoogleAppOnPlayStore(Context context) { 22 | context.startActivity(new Intent(Intent.ACTION_VIEW) 23 | .setData(Uri.parse("market://details?id=" + Speech.GOOGLE_APP_PACKAGE))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/SupportedLanguagesListener.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import java.util.List; 4 | 5 | public interface SupportedLanguagesListener { 6 | void onSupportedLanguages(List supportedLanguages); 7 | void onNotSupported(UnsupportedReason reason); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/TextToSpeechCallback.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | /** 4 | * Contains the methods which are called to notify text to speech progress status. 5 | * 6 | * @author Aleksandar Gotev 7 | */ 8 | public interface TextToSpeechCallback { 9 | void onStart(); 10 | void onCompleted(); 11 | void onError(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/TtsProgressListener.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.speech.tts.UtteranceProgressListener; 6 | 7 | import java.lang.ref.WeakReference; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author Kristiyan Petrov (kristiyan@igenius.net) 12 | */ 13 | 14 | public class TtsProgressListener extends UtteranceProgressListener { 15 | 16 | private final Map mTtsCallbacks; 17 | private final WeakReference contextWeakReference; 18 | 19 | public TtsProgressListener(final Context context, final Map mTtsCallbacks) { 20 | contextWeakReference = new WeakReference<>(context); 21 | this.mTtsCallbacks = mTtsCallbacks; 22 | } 23 | 24 | @Override 25 | public void onStart(final String utteranceId) { 26 | final TextToSpeechCallback callback = mTtsCallbacks.get(utteranceId); 27 | final Context context = contextWeakReference.get(); 28 | 29 | if (callback != null && context != null) { 30 | new Handler(context.getMainLooper()).post(new Runnable() { 31 | @Override 32 | public void run() { 33 | callback.onStart(); 34 | } 35 | }); 36 | } 37 | } 38 | 39 | @Override 40 | public void onDone(final String utteranceId) { 41 | final TextToSpeechCallback callback = mTtsCallbacks.get(utteranceId); 42 | final Context context = contextWeakReference.get(); 43 | if (callback != null && context != null) { 44 | new Handler(context.getMainLooper()).post(new Runnable() { 45 | @Override 46 | public void run() { 47 | callback.onCompleted(); 48 | mTtsCallbacks.remove(utteranceId); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | @Override 55 | public void onError(final String utteranceId) { 56 | final TextToSpeechCallback callback = mTtsCallbacks.get(utteranceId); 57 | final Context context = contextWeakReference.get(); 58 | 59 | if (callback != null && context != null) { 60 | new Handler(context.getMainLooper()).post(new Runnable() { 61 | @Override 62 | public void run() { 63 | callback.onError(); 64 | mTtsCallbacks.remove(utteranceId); 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/UnsupportedReason.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech; 2 | 3 | public enum UnsupportedReason { 4 | GOOGLE_APP_NOT_FOUND, 5 | EMPTY_SUPPORTED_LANGUAGES 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/engine/DummyOnInitListener.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.engine; 2 | 3 | import android.speech.tts.TextToSpeech; 4 | 5 | public class DummyOnInitListener implements TextToSpeech.OnInitListener { 6 | @Override 7 | public void onInit(int status) { 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/engine/SpeechRecognitionEngine.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.engine; 2 | 3 | import android.content.Context; 4 | import android.speech.RecognitionListener; 5 | 6 | import net.gotev.speech.SpeechDelegate; 7 | import net.gotev.speech.GoogleVoiceTypingDisabledException; 8 | import net.gotev.speech.SpeechRecognitionNotAvailable; 9 | import net.gotev.speech.ui.SpeechProgressView; 10 | 11 | import java.util.Locale; 12 | 13 | public interface SpeechRecognitionEngine extends RecognitionListener { 14 | 15 | void init(Context context); 16 | 17 | void clear(); 18 | 19 | String getPartialResultsAsString(); 20 | 21 | void initSpeechRecognizer(Context context); 22 | 23 | void startListening(SpeechProgressView progressView, SpeechDelegate delegate) throws SpeechRecognitionNotAvailable, GoogleVoiceTypingDisabledException; 24 | 25 | void stopListening(); 26 | 27 | void returnPartialResultsAndRecreateSpeechRecognizer(); 28 | 29 | void setPartialResults(boolean getPartialResults); 30 | 31 | void shutdown(); 32 | 33 | boolean isListening(); 34 | 35 | Locale getLocale(); 36 | 37 | void setLocale(Locale locale); 38 | 39 | void setPreferOffline(boolean preferOffline); 40 | 41 | void setTransitionMinimumDelay(long milliseconds); 42 | 43 | void setStopListeningAfterInactivity(long milliseconds); 44 | 45 | void setCallingPackage(String callingPackage); 46 | 47 | void unregisterDelegate(); 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/engine/TextToSpeechEngine.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.engine; 2 | 3 | import android.content.Context; 4 | import android.speech.tts.TextToSpeech; 5 | import android.speech.tts.Voice; 6 | 7 | import net.gotev.speech.TextToSpeechCallback; 8 | 9 | import java.util.List; 10 | import java.util.Locale; 11 | 12 | public interface TextToSpeechEngine { 13 | 14 | void initTextToSpeech(Context context); 15 | 16 | boolean isSpeaking(); 17 | 18 | void say(String message, TextToSpeechCallback callback); 19 | 20 | void stop(); 21 | 22 | void shutdown(); 23 | 24 | void setTextToSpeechQueueMode(int mode); 25 | 26 | void setAudioStream(int audioStream); 27 | 28 | void setOnInitListener(TextToSpeech.OnInitListener onInitListener); 29 | 30 | void setPitch(float pitch); 31 | 32 | void setSpeechRate(float rate); 33 | 34 | void setLocale(Locale locale); 35 | 36 | void setVoice(Voice voice); 37 | 38 | List getSupportedVoices(); 39 | 40 | Voice getCurrentVoice(); 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/SpeechBar.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui; 2 | 3 | import android.graphics.RectF; 4 | 5 | public class SpeechBar { 6 | 7 | private int x; 8 | private int y; 9 | private int radius; 10 | private int height; 11 | 12 | private final int maxHeight; 13 | private final int startX; 14 | private final int startY; 15 | final private RectF rect; 16 | 17 | public SpeechBar(int x, int y, int height, int maxHeight, int radius) { 18 | this.x = x; 19 | this.y = y; 20 | this.radius = radius; 21 | this.startX = x; 22 | this.startY = y; 23 | this.height = height; 24 | this.maxHeight = maxHeight; 25 | this.rect = new RectF(x - radius, 26 | y - height / 2, 27 | x + radius, 28 | y + height / 2); 29 | } 30 | 31 | public void update() { 32 | rect.set(x - radius, 33 | y - height / 2, 34 | x + radius, 35 | y + height / 2); 36 | } 37 | 38 | public int getX() { 39 | return x; 40 | } 41 | 42 | public void setX(int x) { 43 | this.x = x; 44 | } 45 | 46 | public int getY() { 47 | return y; 48 | } 49 | 50 | public void setY(int y) { 51 | this.y = y; 52 | } 53 | 54 | public int getHeight() { 55 | return height; 56 | } 57 | 58 | public void setHeight(int height) { 59 | this.height = height; 60 | } 61 | 62 | public int getMaxHeight() { 63 | return maxHeight; 64 | } 65 | 66 | public int getStartX() { 67 | return startX; 68 | } 69 | 70 | public int getStartY() { 71 | return startY; 72 | } 73 | 74 | public RectF getRect() { 75 | return rect; 76 | } 77 | 78 | public int getRadius() { 79 | return radius; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/BarParamsAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | public interface BarParamsAnimator { 4 | void start(); 5 | void stop(); 6 | void animate(); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/BarRmsAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | import android.view.animation.AccelerateInterpolator; 4 | import android.view.animation.DecelerateInterpolator; 5 | 6 | import net.gotev.speech.ui.SpeechBar; 7 | 8 | import java.util.Random; 9 | 10 | public class BarRmsAnimator implements BarParamsAnimator { 11 | 12 | private static final float QUIT_RMSDB_MAX = 2f; 13 | private static final float MEDIUM_RMSDB_MAX = 5.5f; 14 | 15 | private static final long BAR_ANIMATION_UP_DURATION = 130; 16 | private static final long BAR_ANIMATION_DOWN_DURATION = 500; 17 | final private SpeechBar bar; 18 | private float fromHeightPart; 19 | private float toHeightPart; 20 | private long startTimestamp; 21 | private boolean isPlaying; 22 | private boolean isUpAnimation; 23 | 24 | public BarRmsAnimator(SpeechBar bar) { 25 | this.bar = bar; 26 | } 27 | 28 | @Override 29 | public void start() { 30 | isPlaying = true; 31 | } 32 | 33 | @Override 34 | public void stop() { 35 | isPlaying = false; 36 | } 37 | 38 | @Override 39 | public void animate() { 40 | if (isPlaying) { 41 | update(); 42 | } 43 | } 44 | 45 | public void onRmsChanged(float rmsdB) { 46 | float newHeightPart; 47 | 48 | if (rmsdB < QUIT_RMSDB_MAX) { 49 | newHeightPart = 0.2f; 50 | } else if (rmsdB >= QUIT_RMSDB_MAX && rmsdB <= MEDIUM_RMSDB_MAX) { 51 | newHeightPart = 0.3f + new Random().nextFloat(); 52 | if (newHeightPart > 0.6f) newHeightPart = 0.6f; 53 | } else { 54 | newHeightPart = 0.7f + new Random().nextFloat(); 55 | if (newHeightPart > 1f) newHeightPart = 1f; 56 | 57 | } 58 | 59 | if (newHeightIsSmallerCurrent(newHeightPart)) { 60 | return; 61 | } 62 | 63 | fromHeightPart = (float) bar.getHeight() / bar.getMaxHeight(); 64 | toHeightPart = newHeightPart; 65 | 66 | startTimestamp = System.currentTimeMillis(); 67 | isUpAnimation = true; 68 | isPlaying = true; 69 | } 70 | 71 | private boolean newHeightIsSmallerCurrent(float newHeightPart) { 72 | return (float) bar.getHeight() / bar.getMaxHeight() > newHeightPart; 73 | } 74 | 75 | private void update() { 76 | 77 | long currTimestamp = System.currentTimeMillis(); 78 | long delta = currTimestamp - startTimestamp; 79 | 80 | if (isUpAnimation) { 81 | animateUp(delta); 82 | } else { 83 | animateDown(delta); 84 | } 85 | } 86 | 87 | private void animateUp(long delta) { 88 | boolean finished = false; 89 | int minHeight = (int) (fromHeightPart * bar.getMaxHeight()); 90 | int toHeight = (int) (bar.getMaxHeight() * toHeightPart); 91 | 92 | float timePart = (float) delta / BAR_ANIMATION_UP_DURATION; 93 | 94 | AccelerateInterpolator interpolator = new AccelerateInterpolator(); 95 | int height = minHeight + (int) (interpolator.getInterpolation(timePart) * (toHeight - minHeight)); 96 | 97 | if (height < bar.getHeight()) { 98 | return; 99 | } 100 | 101 | if (height >= toHeight) { 102 | height = toHeight; 103 | finished = true; 104 | } 105 | 106 | bar.setHeight(height); 107 | bar.update(); 108 | 109 | if (finished) { 110 | isUpAnimation = false; 111 | startTimestamp = System.currentTimeMillis(); 112 | } 113 | } 114 | 115 | private void animateDown(long delta) { 116 | int minHeight = bar.getRadius() * 2; 117 | int fromHeight = (int) (bar.getMaxHeight() * toHeightPart); 118 | 119 | float timePart = (float) delta / BAR_ANIMATION_DOWN_DURATION; 120 | 121 | DecelerateInterpolator interpolator = new DecelerateInterpolator(); 122 | int height = minHeight + (int) ((1f - interpolator.getInterpolation(timePart)) * (fromHeight - minHeight)); 123 | 124 | if (height > bar.getHeight()) { 125 | return; 126 | } 127 | 128 | if (height <= minHeight) { 129 | finish(); 130 | return; 131 | } 132 | 133 | bar.setHeight(height); 134 | bar.update(); 135 | } 136 | 137 | private void finish() { 138 | bar.setHeight(bar.getRadius() * 2); 139 | bar.update(); 140 | isPlaying = false; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/IdleAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | import net.gotev.speech.ui.SpeechBar; 4 | 5 | import java.util.List; 6 | 7 | public class IdleAnimator implements BarParamsAnimator { 8 | 9 | private static final long IDLE_DURATION = 1500; 10 | 11 | private long startTimestamp; 12 | private boolean isPlaying; 13 | 14 | private final int floatingAmplitude; 15 | private final List bars; 16 | 17 | public IdleAnimator(List bars, int floatingAmplitude) { 18 | this.floatingAmplitude = floatingAmplitude; 19 | this.bars = bars; 20 | } 21 | 22 | @Override 23 | public void start() { 24 | isPlaying = true; 25 | startTimestamp = System.currentTimeMillis(); 26 | } 27 | 28 | @Override 29 | public void stop() { 30 | isPlaying = false; 31 | } 32 | 33 | @Override 34 | public void animate() { 35 | if (isPlaying) { 36 | update(bars); 37 | } 38 | } 39 | 40 | public void update(List bars) { 41 | 42 | long currTimestamp = System.currentTimeMillis(); 43 | if (currTimestamp - startTimestamp > IDLE_DURATION) { 44 | startTimestamp += IDLE_DURATION; 45 | } 46 | long delta = currTimestamp - startTimestamp; 47 | 48 | int i = 0; 49 | for (SpeechBar bar : bars) { 50 | updateCirclePosition(bar, delta, i); 51 | i++; 52 | } 53 | } 54 | 55 | private void updateCirclePosition(SpeechBar bar, long delta, int num) { 56 | float angle = ((float) delta / IDLE_DURATION) * 360f + 120f * num; 57 | int y = (int) (Math.sin(Math.toRadians(angle)) * floatingAmplitude) + bar.getStartY(); 58 | bar.setY(y); 59 | bar.update(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/RmsAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | import net.gotev.speech.ui.SpeechBar; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class RmsAnimator implements BarParamsAnimator { 9 | final private List barAnimators; 10 | 11 | 12 | public RmsAnimator(List speechBars) { 13 | this.barAnimators = new ArrayList<>(); 14 | for (SpeechBar bar : speechBars) { 15 | barAnimators.add(new BarRmsAnimator(bar)); 16 | } 17 | } 18 | 19 | @Override 20 | public void start() { 21 | for (BarRmsAnimator barAnimator : barAnimators) { 22 | barAnimator.start(); 23 | } 24 | } 25 | 26 | @Override 27 | public void stop() { 28 | for (BarRmsAnimator barAnimator : barAnimators) { 29 | barAnimator.stop(); 30 | } 31 | } 32 | 33 | @Override 34 | public void animate() { 35 | for (BarRmsAnimator barAnimator : barAnimators) { 36 | barAnimator.animate(); 37 | } 38 | } 39 | 40 | public void onRmsChanged(float rmsDB) { 41 | for (BarRmsAnimator barAnimator : barAnimators) { 42 | barAnimator.onRmsChanged(rmsDB); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/RotatingAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | import android.graphics.Point; 4 | import android.view.animation.AccelerateDecelerateInterpolator; 5 | 6 | import net.gotev.speech.ui.SpeechBar; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class RotatingAnimator implements BarParamsAnimator { 12 | 13 | private static final long DURATION = 2000; 14 | private static final long ACCELERATE_ROTATION_DURATION = 1000; 15 | private static final long DECELERATE_ROTATION_DURATION = 1000; 16 | private static final float ROTATION_DEGREES = 720f; 17 | private static final float ACCELERATION_ROTATION_DEGREES = 40f; 18 | 19 | private long startTimestamp; 20 | private boolean isPlaying; 21 | 22 | private final int centerX, centerY; 23 | private final List startPositions; 24 | private final List bars; 25 | 26 | public RotatingAnimator(List bars, int centerX, int centerY) { 27 | this.centerX = centerX; 28 | this.centerY = centerY; 29 | this.bars = bars; 30 | this.startPositions = new ArrayList<>(); 31 | for (SpeechBar bar : bars) { 32 | startPositions.add(new Point(bar.getX(), bar.getY())); 33 | } 34 | } 35 | 36 | @Override 37 | public void start() { 38 | isPlaying = true; 39 | startTimestamp = System.currentTimeMillis(); 40 | } 41 | 42 | @Override 43 | public void stop() { 44 | isPlaying = false; 45 | } 46 | 47 | @Override 48 | public void animate() { 49 | if (!isPlaying) return; 50 | 51 | long currTimestamp = System.currentTimeMillis(); 52 | if (currTimestamp - startTimestamp > DURATION) { 53 | startTimestamp += DURATION; 54 | } 55 | 56 | long delta = currTimestamp - startTimestamp; 57 | 58 | float interpolatedTime = (float) delta / DURATION; 59 | 60 | float angle = interpolatedTime * ROTATION_DEGREES; 61 | 62 | int i = 0; 63 | for (SpeechBar bar : bars) { 64 | float finalAngle = angle; 65 | if (i > 0 && delta > ACCELERATE_ROTATION_DURATION) { 66 | finalAngle += decelerate(delta, bars.size() - i); 67 | } else if (i > 0) { 68 | finalAngle += accelerate(delta, bars.size() - i); 69 | } 70 | rotate(bar, finalAngle, startPositions.get(i)); 71 | i++; 72 | } 73 | } 74 | 75 | private float decelerate(long delta, int scale) { 76 | long accelerationDelta = delta - ACCELERATE_ROTATION_DURATION; 77 | AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); 78 | float interpolatedTime = interpolator.getInterpolation((float) accelerationDelta / DECELERATE_ROTATION_DURATION); 79 | float decelerationAngle = -interpolatedTime * (ACCELERATION_ROTATION_DEGREES * scale); 80 | return ACCELERATION_ROTATION_DEGREES * scale + decelerationAngle; 81 | } 82 | 83 | private float accelerate(long delta, int scale) { 84 | long accelerationDelta = delta; 85 | AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); 86 | float interpolatedTime = interpolator.getInterpolation((float) accelerationDelta / ACCELERATE_ROTATION_DURATION); 87 | float accelerationAngle = interpolatedTime * (ACCELERATION_ROTATION_DEGREES * scale); 88 | return accelerationAngle; 89 | } 90 | 91 | /** 92 | * X = x0 + (x - x0) * cos(a) - (y - y0) * sin(a); 93 | * Y = y0 + (y - y0) * cos(a) + (x - x0) * sin(a); 94 | */ 95 | private void rotate(SpeechBar bar, double degrees, Point startPosition) { 96 | 97 | double angle = Math.toRadians(degrees); 98 | 99 | int x = centerX + (int) ((startPosition.x - centerX) * Math.cos(angle) - 100 | (startPosition.y - centerY) * Math.sin(angle)); 101 | 102 | int y = centerY + (int) ((startPosition.x - centerX) * Math.sin(angle) + 103 | (startPosition.y - centerY) * Math.cos(angle)); 104 | 105 | bar.setX(x); 106 | bar.setY(y); 107 | bar.update(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/net/gotev/speech/ui/animators/TransformAnimator.java: -------------------------------------------------------------------------------- 1 | package net.gotev.speech.ui.animators; 2 | 3 | import android.graphics.Point; 4 | 5 | import net.gotev.speech.ui.SpeechBar; 6 | import net.gotev.speech.ui.SpeechProgressView; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class TransformAnimator implements BarParamsAnimator { 12 | 13 | private static final long DURATION = 300; 14 | 15 | private long startTimestamp; 16 | private boolean isPlaying; 17 | 18 | 19 | private OnInterpolationFinishedListener listener; 20 | 21 | private final int radius; 22 | private final int centerX, centerY; 23 | private final List finalPositions = new ArrayList<>(); 24 | private final List bars; 25 | 26 | public TransformAnimator(List bars, int centerX, int centerY, int radius) { 27 | this.centerX = centerX; 28 | this.centerY = centerY; 29 | this.bars = bars; 30 | this.radius = radius; 31 | } 32 | 33 | @Override 34 | public void start() { 35 | isPlaying = true; 36 | startTimestamp = System.currentTimeMillis(); 37 | initFinalPositions(); 38 | } 39 | 40 | @Override 41 | public void stop() { 42 | isPlaying = false; 43 | if (listener != null) { 44 | listener.onFinished(); 45 | } 46 | } 47 | 48 | @Override 49 | public void animate() { 50 | if (!isPlaying) return; 51 | 52 | long currTimestamp = System.currentTimeMillis(); 53 | long delta = currTimestamp - startTimestamp; 54 | if (delta > DURATION) { 55 | delta = DURATION; 56 | } 57 | 58 | for (int i = 0; i < bars.size(); i++) { 59 | SpeechBar bar = bars.get(i); 60 | 61 | int x = bar.getStartX() + (int) ((finalPositions.get(i).x - bar.getStartX()) * ((float) delta / DURATION)); 62 | int y = bar.getStartY() + (int) ((finalPositions.get(i).y - bar.getStartY()) * ((float) delta / DURATION)); 63 | 64 | bar.setX(x); 65 | bar.setY(y); 66 | bar.update(); 67 | } 68 | 69 | 70 | if (delta == DURATION) { 71 | stop(); 72 | } 73 | } 74 | 75 | private void initFinalPositions() { 76 | Point startPoint = new Point(); 77 | startPoint.x = centerX; 78 | startPoint.y = centerY - radius; 79 | for (int i = 0; i < SpeechProgressView.BARS_COUNT; i++) { 80 | Point point = new Point(startPoint); 81 | rotate((360d / SpeechProgressView.BARS_COUNT) * i, point); 82 | finalPositions.add(point); 83 | } 84 | } 85 | 86 | /** 87 | * X = x0 + (x - x0) * cos(a) - (y - y0) * sin(a); 88 | * Y = y0 + (y - y0) * cos(a) + (x - x0) * sin(a); 89 | **/ 90 | private void rotate(double degrees, Point point) { 91 | 92 | double angle = Math.toRadians(degrees); 93 | 94 | int x = centerX + (int) ((point.x - centerX) * Math.cos(angle) - 95 | (point.y - centerY) * Math.sin(angle)); 96 | 97 | int y = centerY + (int) ((point.x - centerX) * Math.sin(angle) + 98 | (point.y - centerY) * Math.cos(angle)); 99 | 100 | point.x = x; 101 | point.y = y; 102 | } 103 | 104 | public void setOnInterpolationFinishedListener(OnInterpolationFinishedListener listener) { 105 | this.listener = listener; 106 | } 107 | 108 | public interface OnInterpolationFinishedListener { 109 | void onFinished(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/App.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.util.Log 8 | import androidx.appcompat.app.AppCompatDelegate 9 | import androidx.lifecycle.Lifecycle 10 | import androidx.lifecycle.LifecycleEventObserver 11 | import androidx.lifecycle.ProcessLifecycleOwner 12 | import androidx.multidex.MultiDexApplication 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.SupervisorJob 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.launch 18 | import top.rootu.lampa.helpers.Helpers.isConnected 19 | import top.rootu.lampa.helpers.Prefs.appLang 20 | import top.rootu.lampa.helpers.Updater 21 | import top.rootu.lampa.helpers.handleUncaughtException 22 | import top.rootu.lampa.helpers.setLanguage 23 | import top.rootu.lampa.tmdb.TMDB 24 | 25 | class App : MultiDexApplication() { 26 | 27 | private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 28 | 29 | init { 30 | // use vectors on pre-LP devices 31 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 32 | } 33 | 34 | companion object { 35 | private val TAG: String = App::class.java.simpleName 36 | private lateinit var appContext: Context 37 | 38 | @Volatile 39 | var inForeground: Boolean = false 40 | private set 41 | 42 | val context: Context 43 | get() = appContext 44 | 45 | private val lifecycleEventObserver = LifecycleEventObserver { _, event -> 46 | inForeground = when (event) { 47 | Lifecycle.Event.ON_START -> { 48 | if (BuildConfig.DEBUG) Log.d(TAG, "in foreground") 49 | true 50 | } 51 | 52 | Lifecycle.Event.ON_STOP -> { 53 | if (BuildConfig.DEBUG) Log.d(TAG, "in background") 54 | false 55 | } 56 | 57 | else -> inForeground 58 | } 59 | } 60 | 61 | fun toast(txt: String, long: Boolean = true) { 62 | if (Looper.myLooper() == Looper.getMainLooper()) { 63 | showToast(txt, long) 64 | } else { 65 | Handler(Looper.getMainLooper()).post { showToast(txt, long) } 66 | } 67 | } 68 | 69 | fun toast(txt: Int, long: Boolean = true) { 70 | if (Looper.myLooper() == Looper.getMainLooper()) { 71 | showToast(context.getString(txt), long) 72 | } else { 73 | Handler(Looper.getMainLooper()).post { 74 | showToast(context.getString(txt), long) 75 | } 76 | } 77 | } 78 | 79 | private fun showToast(text: String, long: Boolean) { 80 | val duration = if (long) android.widget.Toast.LENGTH_LONG 81 | else android.widget.Toast.LENGTH_SHORT 82 | android.widget.Toast.makeText(appContext, text, duration).show() 83 | } 84 | 85 | fun setAppLanguage(context: Context, langCode: String) { 86 | context.appLang = langCode 87 | if (context is BaseActivity) { 88 | context.recreateWithLanguage() 89 | } 90 | } 91 | } 92 | 93 | override fun onCreate() { 94 | super.onCreate() 95 | // setup applicationContext 96 | appContext = applicationContext.setLanguage() 97 | // ensure resources are properly initialized 98 | resources 99 | 100 | // register lifecycle observer 101 | ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleEventObserver) 102 | 103 | // app crash handler 104 | handleUncaughtException(showLogs = true) 105 | //CrashHandler(this).initialize(showLogs = BuildConfig.DEBUG) 106 | 107 | // Initialize components 108 | initializeComponents() 109 | } 110 | 111 | private fun initializeComponents() { 112 | // self-update check 113 | if (BuildConfig.enableUpdate) { 114 | applicationScope.launch { 115 | checkForUpdates() 116 | } 117 | } 118 | 119 | // Init TMDB genres 120 | applicationScope.launch { 121 | TMDB.initGenres() 122 | } 123 | 124 | } 125 | 126 | private suspend fun checkForUpdates() { 127 | var count = 60 128 | try { 129 | while (!isConnected(appContext) && count > 0) { 130 | delay(1000) // wait for network 131 | count-- 132 | } 133 | 134 | if (count > 0 && Updater.check()) { 135 | while (count > 0 && !inForeground) { // wait foreground 136 | delay(1000) 137 | count-- 138 | } 139 | if (inForeground) { 140 | val intent = Intent(appContext, UpdateActivity::class.java).apply { 141 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 142 | } 143 | startActivity(intent) 144 | } 145 | } 146 | } catch (e: Exception) { 147 | Log.e(TAG, "Update check failed", e) 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/AppListAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.content.pm.ResolveInfo 6 | import android.graphics.drawable.Drawable 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.BaseAdapter 11 | import android.widget.ImageView 12 | import android.widget.TextView 13 | import java.util.Locale 14 | 15 | class AppListAdapter internal constructor( 16 | context: Context, 17 | private val appsInfo: List 18 | ) : BaseAdapter() { 19 | private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context) 20 | private val pm: PackageManager = context.packageManager 21 | 22 | 23 | override fun getCount(): Int { 24 | return appsInfo.size 25 | } 26 | 27 | override fun getItem(position: Int): ResolveInfo { 28 | return appsInfo[position] 29 | } 30 | 31 | override fun getItemId(position: Int): Long { 32 | return position.toLong() 33 | } 34 | 35 | override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { 36 | var view = convertView 37 | if (view == null) 38 | view = mLayoutInflater.inflate(R.layout.app_list_item, null) 39 | val image = view?.findViewById(R.id.imageViewIcon) 40 | val textViewMain = view?.findViewById(R.id.textViewMain) 41 | val textViewSecond = view?.findViewById(R.id.textViewSecond) 42 | image?.setImageDrawable(getItemIcon(position)) 43 | textViewMain?.text = getItemLabel(position) 44 | textViewSecond?.text = getItemPackage(position) 45 | return view!! 46 | } 47 | 48 | private fun getItemIcon(position: Int): Drawable? { 49 | return getItem(position).loadIcon(pm) 50 | } 51 | 52 | private fun getItemLabel(position: Int): String { 53 | val loadLabel = getItem(position).loadLabel(pm) 54 | var label = "" 55 | if (loadLabel == null || loadLabel.toString().also { label = it }.isEmpty()) { 56 | label = getItemPackage(position) 57 | } 58 | return label 59 | } 60 | 61 | fun getItemPackage(position: Int): String { 62 | return getItem(position).activityInfo.packageName.lowercase(Locale.getDefault()) 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/AsyncTask.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import kotlinx.coroutines.* 6 | import java.util.concurrent.Executors 7 | 8 | abstract class AsyncTask(private val taskName: String) { 9 | 10 | enum class Status { 11 | PENDING, 12 | RUNNING, 13 | FINISHED 14 | } 15 | 16 | companion object { 17 | private var threadPoolExecutor: CoroutineDispatcher? = null 18 | } 19 | 20 | var isCancelled = false 21 | var status: Status = Status.PENDING 22 | private var preJob: Job? = null 23 | private var bgJob: Deferred? = null 24 | 25 | private val className by lazy { 26 | AsyncTask::class.java.simpleName 27 | } 28 | 29 | abstract fun doInBackground(vararg params: Params?): Result 30 | open fun onProgressUpdate(vararg values: Progress?) {} 31 | open fun onPostExecute(result: Result?) {} 32 | open fun onPreExecute() {} 33 | open fun onCancelled(result: Result?) {} 34 | 35 | /** 36 | * Executes background task parallel with other background tasks in the queue using 37 | * default thread pool 38 | */ 39 | fun execute(vararg params: Params?) { 40 | execute(Dispatchers.Default, *params) 41 | } 42 | 43 | /** 44 | * Executes background tasks sequentially with other background tasks in the queue using 45 | * single thread executor @Executors.newSingleThreadExecutor(). 46 | */ 47 | fun executeOnExecutor(vararg params: Params?) { 48 | if (threadPoolExecutor == null) { 49 | threadPoolExecutor = Executors.newSingleThreadExecutor().asCoroutineDispatcher() 50 | } 51 | execute(threadPoolExecutor!!, *params) 52 | } 53 | 54 | private fun execute(dispatcher: CoroutineDispatcher, vararg params: Params?) { 55 | 56 | if (status != Status.PENDING) { 57 | when (status) { 58 | Status.RUNNING -> throw IllegalStateException("Cannot execute task:" + " the task is already running.") 59 | Status.FINISHED -> throw IllegalStateException( 60 | "Cannot execute task:" 61 | + " the task has already been executed " 62 | + "(a task can be executed only once)" 63 | ) 64 | 65 | else -> { 66 | } 67 | } 68 | } 69 | 70 | status = Status.RUNNING 71 | 72 | // it can be used to setup UI - it should have access to Main Thread 73 | CoroutineScope(Dispatchers.Main).launch { 74 | preJob = launch(Dispatchers.Main) { 75 | printLog("$taskName onPreExecute started") 76 | onPreExecute() 77 | printLog("$taskName onPreExecute finished") 78 | bgJob = async(dispatcher) { 79 | printLog("$taskName doInBackground started") 80 | doInBackground(*params) 81 | } 82 | } 83 | preJob!!.join() 84 | if (!isCancelled) { 85 | withContext(Dispatchers.Main) { 86 | onPostExecute(bgJob!!.await()) 87 | printLog("$taskName doInBackground finished") 88 | status = Status.FINISHED 89 | } 90 | } 91 | } 92 | } 93 | 94 | fun cancel(mayInterruptIfRunning: Boolean) { 95 | if (preJob == null || bgJob == null) { 96 | printLog("$taskName has already been cancelled/finished/not yet started.") 97 | return 98 | } 99 | if (mayInterruptIfRunning || (!preJob!!.isActive && !bgJob!!.isActive)) { 100 | isCancelled = true 101 | status = Status.FINISHED 102 | if (bgJob!!.isCompleted) { 103 | CoroutineScope(Dispatchers.Main).launch { 104 | onCancelled(bgJob!!.await()) 105 | } 106 | } 107 | preJob?.cancel(CancellationException("PreExecute: Coroutine Task cancelled")) 108 | bgJob?.cancel(CancellationException("doInBackground: Coroutine Task cancelled")) 109 | printLog("$taskName has been cancelled.") 110 | } 111 | } 112 | 113 | fun publishProgress(vararg progress: Progress) { 114 | //need to update main thread 115 | CoroutineScope(Dispatchers.Main).launch { 116 | if (!isCancelled) { 117 | onProgressUpdate(*progress) 118 | } 119 | } 120 | } 121 | 122 | private fun printLog(message: String) { 123 | if (BuildConfig.DEBUG) Log.d(className, message) 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/AutoCompleteTV.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.Rect 7 | import android.graphics.drawable.ColorDrawable 8 | import android.text.InputType 9 | import android.util.AttributeSet 10 | import android.view.ViewGroup 11 | import android.view.inputmethod.InputMethodManager 12 | import android.widget.AutoCompleteTextView 13 | import android.widget.ListPopupWindow 14 | import android.widget.ListView 15 | import androidx.appcompat.widget.AppCompatAutoCompleteTextView 16 | import androidx.core.view.updateLayoutParams 17 | import top.rootu.lampa.helpers.Helpers.dp2px 18 | import top.rootu.lampa.helpers.Prefs.remUrlHistory 19 | import top.rootu.lampa.helpers.isAmazonDev 20 | 21 | // Extension of AppCompatAutoCompleteTextView that automatically hides the soft keyboard 22 | // when the autocomplete dropdown is touched. 23 | 24 | class AutoCompleteTV @JvmOverloads constructor( 25 | context: Context, 26 | attributeSet: AttributeSet? = null, 27 | defStyleAttr: Int = androidx.appcompat.R.attr.autoCompleteTextViewStyle 28 | ) : AppCompatAutoCompleteTextView(context, attributeSet, defStyleAttr) { 29 | 30 | companion object { 31 | // need to access private field ('mPopup') of AutoCompleteTextView 32 | @SuppressLint("PrivateApi") 33 | private val popupWindowField = AutoCompleteTextView::class.java.getDeclaredField("mPopup") 34 | .also { it.isAccessible = true } 35 | } 36 | 37 | init { 38 | // Set input type to enable action button (Done/Next/Enter) 39 | setRawInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) 40 | setOnClickListener { 41 | dismissDropDown() 42 | showKeyboard() 43 | } 44 | } 45 | 46 | var onPopupVisibilityChanged: ((Boolean) -> Unit)? = null 47 | 48 | private val popupWindow = popupWindowField.get(this) as? ListPopupWindow? 49 | private val fadingEdgeH = dp2px(this.context, 64.0F) 50 | 51 | private fun hideKeyboard() { 52 | val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager 53 | imm?.hideSoftInputFromWindow(rootView.windowToken, 0) 54 | } 55 | 56 | private fun showKeyboard() { 57 | val context = context ?: return 58 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager 59 | if (context.isAmazonDev) 60 | imm?.showSoftInput(this, 0) // FireTV keyboard doesn't like SHOW_IMPLICIT 61 | else 62 | imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) 63 | } 64 | 65 | private fun setListViewBasedOnChildren(listView: ListView) { 66 | listView.updateLayoutParams { 67 | width = ViewGroup.LayoutParams.WRAP_CONTENT 68 | } 69 | } 70 | 71 | // add scroll listener to dropdown in order to hide IME when scrolled 72 | @SuppressLint("ClickableViewAccessibility") 73 | override fun showDropDown() { 74 | super.showDropDown() 75 | val itemsCount = popupWindow?.listView?.count ?: -1 76 | onPopupVisibilityChanged?.invoke(itemsCount > 0) 77 | // ajust vertical dropdown offset 78 | this.dropDownVerticalOffset = resources.getDimensionPixelSize(R.dimen.dropdown_margin_top) 79 | // hide scrollbar and add fading edge 80 | popupWindow?.listView?.apply { 81 | isVerticalScrollBarEnabled = false 82 | isVerticalFadingEdgeEnabled = true 83 | setFadingEdgeLength(fadingEdgeH) 84 | background = ColorDrawable(Color.TRANSPARENT) 85 | overScrollMode = OVER_SCROLL_NEVER 86 | // update listview layout 87 | setListViewBasedOnChildren(this) 88 | } 89 | // the popup list view shouldn't be null here because the dropdown was just shown 90 | // ... perhaps unless it's empty 91 | popupWindow?.listView?.setOnTouchListener { _, _ -> 92 | hideKeyboard() 93 | // have to return false here otherwise scrolling won't work 94 | false 95 | } 96 | // handle remove url from history 97 | popupWindow?.listView?.setOnItemLongClickListener { _, view, position, _ -> 98 | val url = adapter.getItem(position) as String? 99 | if (!url.isNullOrEmpty()) { 100 | view.context.remUrlHistory(url) // update Prefs 101 | MainActivity.urlAdapter.remove(url) // update GUI 102 | } 103 | true 104 | } 105 | } 106 | 107 | override fun dismissDropDown() { 108 | super.dismissDropDown() 109 | onPopupVisibilityChanged?.invoke(false) 110 | } 111 | 112 | override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { 113 | super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) 114 | if (gainFocus) { 115 | // Handle focus gained 116 | showDropDown() 117 | } else { 118 | // Handle focus lost 119 | dismissDropDown() 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import top.rootu.lampa.helpers.setLanguage 8 | 9 | abstract class BaseActivity : AppCompatActivity() { 10 | override fun attachBaseContext(newBase: Context) { 11 | super.attachBaseContext(newBase.setLanguage()) 12 | } 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | // Ensure resources are properly initialized 17 | resources 18 | } 19 | 20 | fun recreateWithLanguage() { 21 | val intent = Intent(this, this::class.java).apply { 22 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 23 | putExtra("lang_changed", true) 24 | } 25 | startActivity(intent) 26 | finish() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/ImgArrayAdapter.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Build 6 | import android.util.TypedValue 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.ArrayAdapter 10 | import android.widget.ListView 11 | import android.widget.TextView 12 | import androidx.core.content.ContextCompat 13 | 14 | @SuppressLint("ResourceType") 15 | class ImgArrayAdapter( 16 | context: Context, 17 | items: List, 18 | val images: List, 19 | layoutRes: Int = android.R.layout.select_dialog_item 20 | ) : ArrayAdapter(context, layoutRes, items) { 21 | 22 | private var selectedItem = 0 23 | private val drawablePaddingPx = TypedValue.applyDimension( 24 | TypedValue.COMPLEX_UNIT_DIP, 25 | 12f, 26 | context.resources.displayMetrics 27 | ).toInt() 28 | private val activeBg = ContextCompat.getDrawable(context, R.drawable.active_menu_bg) 29 | private val transparentColor = ContextCompat.getColor(context, android.R.color.transparent) 30 | 31 | companion object { 32 | fun create( 33 | context: Context, 34 | items: List, 35 | images: List, 36 | layoutRes: Int = android.R.layout.select_dialog_item 37 | ) = ImgArrayAdapter(context, items, images, layoutRes) 38 | 39 | fun fromResources( 40 | context: Context, 41 | textArrayRes: Int, 42 | imageArrayRes: Int 43 | ): ImgArrayAdapter { 44 | val texts = context.resources.getTextArray(textArrayRes).map { it?.toString() } 45 | val typedArray = context.resources.obtainTypedArray(imageArrayRes) 46 | val images = try { 47 | List(typedArray.length()) { i -> typedArray.getResourceId(i, -1) } 48 | } finally { 49 | typedArray.recycle() 50 | } 51 | return create(context, texts, images) 52 | } 53 | } 54 | 55 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 56 | val view = super.getView(position, convertView, parent).apply { 57 | findViewById(android.R.id.text1)?.configureTextView(position) 58 | if (position == selectedItem) { 59 | // Set focus on selected position 60 | (parent as ListView).setSelectionFromTop(position, top) 61 | } 62 | } 63 | return view 64 | } 65 | 66 | private fun TextView.configureTextView(position: Int) { 67 | val drawableRes = images.getOrNull(position) ?: return 68 | val drawable = ContextCompat.getDrawable(context, drawableRes) 69 | 70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 71 | setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) 72 | } else { 73 | setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) 74 | } 75 | compoundDrawablePadding = drawablePaddingPx 76 | textSize = 18f 77 | maxLines = 2 78 | if (position == selectedItem) { 79 | background = activeBg 80 | } else { 81 | setBackgroundColor(transparentColor) 82 | } 83 | } 84 | 85 | fun setSelectedItem(position: Int) { 86 | if (selectedItem != position) { 87 | selectedItem = position 88 | notifyDataSetChanged() 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/LampaGlideModule.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import com.bumptech.glide.Glide 6 | import com.bumptech.glide.Registry 7 | import com.bumptech.glide.annotation.Excludes 8 | import com.bumptech.glide.annotation.GlideModule 9 | import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule 10 | import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader 11 | import com.bumptech.glide.load.model.GlideUrl 12 | import com.bumptech.glide.module.AppGlideModule 13 | import top.rootu.lampa.tmdb.TMDB.permissiveOkHttp 14 | import top.rootu.lampa.tmdb.TMDB.startWithQuad9DNS 15 | import java.io.InputStream 16 | 17 | @GlideModule 18 | @Excludes(OkHttpLibraryGlideModule::class) 19 | class LampaGlideModule : AppGlideModule() { 20 | override fun isManifestParsingEnabled(): Boolean { 21 | return false 22 | } 23 | 24 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 25 | val client = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 26 | startWithQuad9DNS() else permissiveOkHttp() 27 | val factory = OkHttpUrlLoader.Factory(client) 28 | registry.replace( 29 | GlideUrl::class.java, 30 | InputStream::class.java, 31 | factory 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/LoadingUtils.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.content.Context 4 | 5 | class LoadingUtils { 6 | 7 | companion object { 8 | private var lottieLoader: LottieLoader? = null 9 | fun showDialog( 10 | context: Context?, 11 | isCancelable: Boolean 12 | ) { 13 | hideDialog() 14 | if (context != null) { 15 | try { 16 | lottieLoader = LottieLoader(context) 17 | lottieLoader?.let { loader -> 18 | loader.setCanceledOnTouchOutside(true) 19 | loader.setCancelable(isCancelable) 20 | loader.show() 21 | } 22 | 23 | } catch (e: Exception) { 24 | e.printStackTrace() 25 | } 26 | } 27 | } 28 | 29 | fun hideDialog() { 30 | if (lottieLoader != null && lottieLoader?.isShowing!!) { 31 | lottieLoader = try { 32 | lottieLoader?.dismiss() 33 | null 34 | } catch (e: Exception) { 35 | null 36 | } 37 | } 38 | } 39 | 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/LottieLoader.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.widget.LinearLayout 7 | 8 | class LottieLoader(context: Context): Dialog(context) { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.loader) 12 | window?.setLayout( 13 | LinearLayout.LayoutParams.MATCH_PARENT, 14 | LinearLayout.LayoutParams.MATCH_PARENT 15 | ) 16 | window?.setBackgroundDrawableResource(android.R.color.transparent) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/browser/XWalk.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.browser 2 | 3 | import android.view.View 4 | import org.xwalk.core.XWalkResourceClient 5 | import org.xwalk.core.XWalkView 6 | import top.rootu.lampa.App 7 | import top.rootu.lampa.MainActivity 8 | import top.rootu.lampa.R 9 | import top.rootu.lampa.helpers.getNetworkErrorString 10 | 11 | class XWalk(override val mainActivity: MainActivity, override val viewResId: Int) : Browser { 12 | private var browser: XWalkView? = null 13 | override var isDestroyed = false 14 | override fun initialize() { 15 | if (browser == null) { 16 | browser = mainActivity.findViewById(viewResId) 17 | browser?.let { xWalkView -> 18 | xWalkView.setLayerType(View.LAYER_TYPE_NONE, null) 19 | xWalkView.setResourceClient(object : XWalkResourceClient(xWalkView) { 20 | override fun onLoadFinished(view: XWalkView, url: String) { 21 | super.onLoadFinished(view, url) 22 | mainActivity.onBrowserPageFinished(view, url) 23 | } 24 | 25 | override fun onReceivedLoadError( 26 | view: XWalkView?, 27 | errorCode: Int, 28 | description: String?, 29 | failingUrl: String? 30 | ) { 31 | super.onReceivedLoadError(view, errorCode, description, failingUrl) 32 | if (failingUrl.toString().trimEnd('/') 33 | .equals(MainActivity.LAMPA_URL, true) 34 | ) { 35 | val reason = App.context.getNetworkErrorString(description.toString()) 36 | val msg = 37 | "${view?.context?.getString(R.string.download_failed_message)} ${MainActivity.LAMPA_URL} – $reason" 38 | mainActivity.showUrlInputDialog(msg) 39 | } 40 | } 41 | }) 42 | mainActivity.onBrowserInitCompleted() 43 | } 44 | } 45 | } 46 | 47 | override fun setUserAgentString(ua: String?) { 48 | browser?.userAgentString = ua 49 | } 50 | 51 | override fun getUserAgentString(): String? { 52 | return browser?.userAgentString 53 | } 54 | 55 | override fun addJavascriptInterface(jsObject: Any, name: String) { 56 | browser?.addJavascriptInterface(jsObject, name) 57 | } 58 | 59 | override fun loadUrl(url: String) { 60 | browser?.loadUrl(url) 61 | } 62 | 63 | override fun pauseTimers() { 64 | if (!isDestroyed) 65 | browser?.pauseTimers() 66 | } 67 | 68 | override fun resumeTimers() { 69 | browser?.resumeTimers() 70 | } 71 | 72 | override fun evaluateJavascript(script: String, resultCallback: (String) -> Unit) { 73 | browser?.evaluateJavascript(script, resultCallback) 74 | } 75 | 76 | override fun clearCache(includeDiskFiles: Boolean) { 77 | browser?.clearCache(includeDiskFiles) 78 | } 79 | 80 | override fun destroy() { 81 | browser?.onDestroy() 82 | isDestroyed = true 83 | } 84 | 85 | override fun setBackgroundColor(color: Int) { 86 | browser?.setBackgroundColor(color) 87 | } 88 | 89 | override fun canGoBack(): Boolean { 90 | return false 91 | } 92 | 93 | override fun goBack() {} 94 | 95 | override fun setFocus() {} 96 | 97 | override fun getView(): View? { 98 | return browser 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/channels/LampaChannels.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.channels 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import androidx.annotation.RequiresApi 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.launch 10 | import top.rootu.lampa.BuildConfig 11 | import top.rootu.lampa.content.LampaProvider 12 | import top.rootu.lampa.helpers.Helpers.isTvContentProviderAvailable 13 | 14 | object LampaChannels { 15 | private const val TAG = "LampaChannels" 16 | private val lock = Any() 17 | private const val MAX_RECS_CAP = 30 18 | 19 | @RequiresApi(Build.VERSION_CODES.O) 20 | fun update(sync: Boolean = true) { 21 | if (!isTvContentProviderAvailable) return 22 | 23 | synchronized(lock) { 24 | if (BuildConfig.DEBUG) Log.d(TAG, "update(sync: $sync)") 25 | 26 | // List of channel names and their corresponding update functions 27 | val channels = listOf( 28 | LampaProvider.RECS to { 29 | LampaProvider.get(LampaProvider.RECS, true)?.items.orEmpty().take(MAX_RECS_CAP) 30 | }, 31 | LampaProvider.LIKE to { 32 | LampaProvider.get( 33 | LampaProvider.LIKE, 34 | false 35 | )?.items.orEmpty() 36 | }, 37 | LampaProvider.BOOK to { 38 | LampaProvider.get( 39 | LampaProvider.BOOK, 40 | false 41 | )?.items.orEmpty() 42 | }, 43 | LampaProvider.HIST to { 44 | LampaProvider.get( 45 | LampaProvider.HIST, 46 | false 47 | )?.items.orEmpty() 48 | }, 49 | LampaProvider.LOOK to { 50 | LampaProvider.get( 51 | LampaProvider.LOOK, 52 | false 53 | )?.items.orEmpty() 54 | }, 55 | LampaProvider.VIEW to { 56 | LampaProvider.get( 57 | LampaProvider.VIEW, 58 | false 59 | )?.items.orEmpty() 60 | }, 61 | LampaProvider.SCHD to { 62 | LampaProvider.get( 63 | LampaProvider.SCHD, 64 | false 65 | )?.items.orEmpty() 66 | }, 67 | LampaProvider.CONT to { 68 | LampaProvider.get( 69 | LampaProvider.CONT, 70 | false 71 | )?.items.orEmpty() 72 | }, 73 | LampaProvider.THRW to { 74 | LampaProvider.get( 75 | LampaProvider.THRW, 76 | false 77 | )?.items.orEmpty() 78 | } 79 | ) 80 | 81 | if (!sync) { 82 | // Use coroutines to update data concurrently 83 | CoroutineScope(Dispatchers.Default).launch { 84 | val deferredResults = channels.map { (name, fetchFunction) -> 85 | async { name to fetchFunction() } 86 | } 87 | deferredResults.forEach { deferred -> 88 | val (name, items) = deferred.await() 89 | ChannelManager.update(name, items) 90 | } 91 | // Update WatchNext after all channels are updated 92 | WatchNext.updateWatchNext() 93 | } 94 | } else { 95 | // Fetch data sequentially 96 | channels.forEach { (name, fetchFunction) -> 97 | val items = fetchFunction() 98 | ChannelManager.update(name, items) 99 | } 100 | // Update WatchNext after all channels are updated 101 | CoroutineScope(Dispatchers.Default).launch { 102 | WatchNext.updateWatchNext() 103 | } 104 | } 105 | } 106 | } 107 | 108 | @RequiresApi(Build.VERSION_CODES.O) 109 | fun updateRecsChannel() { 110 | if (!isTvContentProviderAvailable) return 111 | synchronized(lock) { 112 | if (BuildConfig.DEBUG) Log.d(TAG, "updateRecsChannel()") 113 | val list = 114 | LampaProvider.get(LampaProvider.RECS, true)?.items.orEmpty().take(MAX_RECS_CAP) 115 | ChannelManager.update(LampaProvider.RECS, list) 116 | } 117 | } 118 | 119 | @RequiresApi(Build.VERSION_CODES.O) 120 | fun updateChanByName(name: String) { 121 | if (!isTvContentProviderAvailable) return 122 | synchronized(lock) { 123 | if (BuildConfig.DEBUG) Log.d(TAG, "updateChanByName($name)") 124 | val list = LampaProvider.get(name, false)?.items.orEmpty() 125 | ChannelManager.update(name, list) 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Bookmarks.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.bookToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Bookmarks : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Bookmarks.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.BOOK } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.book?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.book?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.bookToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Continued.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.contToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Continued : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Continued.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.CONT } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.continued?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.continued?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.contToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/History.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.histToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class History : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(History.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.HIST } 22 | ?.sortedByDescending { it.time } 23 | ?.mapNotNull { it.data?.apply { fixCard() } } 24 | ?.let { lst.addAll(it) } 25 | } else { // FAV 26 | App.context.FAV?.card 27 | ?.filter { App.context.FAV?.history?.contains(it.id.toString()) == true } 28 | ?.sortedBy { App.context.FAV?.history?.indexOf(it.id) } 29 | ?.let { lst.addAll(it) } 30 | } 31 | // Exclude pending 32 | return lst 33 | .filterNot { App.context.histToRemove.contains(it.id.toString()) } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/LampaProvider.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import android.util.Log 4 | import top.rootu.lampa.App 5 | import top.rootu.lampa.BuildConfig 6 | import top.rootu.lampa.helpers.Prefs.CUB 7 | import top.rootu.lampa.helpers.Prefs.FAV 8 | import top.rootu.lampa.helpers.Prefs.syncEnabled 9 | import top.rootu.lampa.models.LampaCard 10 | 11 | abstract class LampaProviderI { 12 | abstract fun get(): LampaContent? 13 | } 14 | 15 | data class LampaContent( 16 | val items: List? 17 | ) 18 | 19 | object LampaProvider { 20 | // this is channel internal id 21 | const val RECS = "recs" 22 | const val BOOK = "book" 23 | const val LATE = "wath" 24 | const val LIKE = "like" 25 | const val HIST = "history" 26 | const val LOOK = "look" 27 | const val VIEW = "viewed" 28 | const val SCHD = "scheduled" 29 | const val CONT = "continued" 30 | const val THRW = "thrown" 31 | 32 | // Map of provider names to provider instances 33 | private val providers = mapOf( 34 | RECS to Recs(), 35 | BOOK to Bookmarks(), 36 | LIKE to Like(), 37 | HIST to History(), 38 | LOOK to Look(), 39 | VIEW to Viewed(), 40 | SCHD to Scheduled(), 41 | CONT to Continued(), 42 | THRW to Thrown(), 43 | ) 44 | 45 | /** 46 | * Retrieves content from the specified provider. 47 | * 48 | * @param name The name of the provider. 49 | * @param filter Whether to filter out viewed or historical content. 50 | * @return A LampaContent object containing the content, or null if an error occurs. 51 | */ 52 | fun get(name: String, filter: Boolean): LampaContent? { 53 | val provider = providers[name] ?: return null 54 | return synchronized(provider) { 55 | try { 56 | val release = provider.get() ?: return@synchronized null 57 | if (filter) { 58 | LampaContent(filterViewed(release.items.orEmpty())) 59 | } else { 60 | release 61 | } 62 | } catch (e: Exception) { 63 | Log.e("LampaProvider", "Error retrieving content from provider $name", e) 64 | null 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Filters out content that has been viewed or is in the history. 71 | * 72 | * @param lst The list of LampaCard objects to filter. 73 | * @return A filtered list of LampaCard objects. 74 | */ 75 | private fun filterViewed(lst: List): List { 76 | val fav = App.context.FAV 77 | val cub = App.context.CUB 78 | val syncEnabled = App.context.syncEnabled 79 | 80 | // Precompute sets of IDs for faster lookup 81 | val favHistoryIds = if (!syncEnabled) fav?.history?.toSet() ?: emptySet() else emptySet() 82 | val favViewedIds = if (!syncEnabled) fav?.viewed?.toSet() ?: emptySet() else emptySet() 83 | val cubHistoryIds = 84 | if (syncEnabled) cub?.filter { it.type == HIST }?.mapNotNull { it.card_id }?.toSet() 85 | ?: emptySet() else emptySet() 86 | val cubViewedIds = 87 | if (syncEnabled) cub?.filter { it.type == VIEW }?.mapNotNull { it.card_id }?.toSet() 88 | ?: emptySet() else emptySet() 89 | if (BuildConfig.DEBUG) Log.d("LampaProvider", "filterViewed favHistoryIds: $favHistoryIds") 90 | if (BuildConfig.DEBUG) Log.d("LampaProvider", "filterViewed favViewedIds: $favViewedIds") 91 | if (BuildConfig.DEBUG) Log.d("LampaProvider", "filterViewed cubHistoryIds: $cubHistoryIds") 92 | if (BuildConfig.DEBUG) Log.d("LampaProvider", "filterViewed cubViewedIds: $cubViewedIds") 93 | 94 | return lst.filter { card -> 95 | val isInHistory = card.id in favHistoryIds || card.id in cubHistoryIds 96 | val isInViewed = card.id in favViewedIds || card.id in cubViewedIds 97 | if (isInHistory || isInViewed) { 98 | if (BuildConfig.DEBUG) Log.d( 99 | "LampaProvider", 100 | "filterViewed Excluded card: ${card.id} (in history: $isInHistory, in viewed: $isInViewed)" 101 | ) 102 | } 103 | !isInHistory && !isInViewed 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Like.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.likeToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Like : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Like.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.LIKE } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.like?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.like?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.likeToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Look.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.lookToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Look : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Look.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.LOOK } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.look?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.look?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.lookToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Recs.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import android.util.Log 4 | import top.rootu.lampa.App 5 | import top.rootu.lampa.BuildConfig 6 | import top.rootu.lampa.helpers.Prefs.REC 7 | import top.rootu.lampa.models.LampaCard 8 | import top.rootu.lampa.models.LampaRec 9 | 10 | class Recs : LampaProviderI() { 11 | override fun get(): LampaContent { 12 | return LampaContent(Recs.get()) 13 | } 14 | 15 | companion object { 16 | fun get(): List { 17 | val lst = mutableListOf() 18 | val recommendations = App.context.REC ?: return lst // Handle null case 19 | // Apply filters, ensure uniqueness, and shuffle 20 | val filtered = recommendations 21 | .filterAll(generateFilters()) 22 | .distinctBy { it.id } 23 | .shuffled() 24 | // Log results in debug mode 25 | if (BuildConfig.DEBUG) { 26 | Log.d( 27 | "Recs", 28 | "Total recommendations: ${recommendations.size} | Filtered: ${filtered.size}" 29 | ) 30 | } 31 | // Convert filtered recommendations to LampaCard and add to the list 32 | filtered.forEach { recommendation -> 33 | lst.add(recommendation.toLampaCard()) 34 | } 35 | return lst 36 | } 37 | 38 | private fun generateFilters(): List<(LampaRec) -> Boolean> = listOf( 39 | // { it.genre_ids?.contains("16") != true }, // Exclude Animation 40 | { it.vote_average?.let { rating -> rating > 6 } == true }, // Rating > 6 41 | { it.popularity?.let { pop -> pop > 4 } == true } // Popularity > 4 42 | ) 43 | 44 | private fun List.filterAll(filters: List<(T) -> Boolean>): List = 45 | filter { item -> filters.all { filter -> filter(item) } } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Scheduled.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.schdToRemove 7 | import top.rootu.lampa.helpers.Prefs.syncEnabled 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Scheduled : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Scheduled.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.SCHD } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.scheduled?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.scheduled?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.schdToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Thrown.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.syncEnabled 7 | import top.rootu.lampa.helpers.Prefs.thrwToRemove 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Thrown : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Thrown.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.THRW } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.thrown?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.thrown?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.thrwToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/content/Viewed.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.content 2 | 3 | import top.rootu.lampa.App 4 | import top.rootu.lampa.helpers.Prefs.CUB 5 | import top.rootu.lampa.helpers.Prefs.FAV 6 | import top.rootu.lampa.helpers.Prefs.syncEnabled 7 | import top.rootu.lampa.helpers.Prefs.viewToRemove 8 | import top.rootu.lampa.models.LampaCard 9 | 10 | class Viewed : LampaProviderI() { 11 | 12 | override fun get(): LampaContent { 13 | return LampaContent(Viewed.get()) 14 | } 15 | 16 | companion object { 17 | fun get(): List { 18 | val lst = mutableListOf() 19 | if (App.context.syncEnabled) { // CUB 20 | App.context.CUB 21 | ?.filter { it.type == LampaProvider.VIEW } 22 | ?.mapNotNull { it.data?.apply { fixCard() } } 23 | ?.let { lst.addAll(it) } 24 | } else { // FAV 25 | App.context.FAV?.card 26 | ?.filter { App.context.FAV?.viewed?.contains(it.id.toString()) == true } 27 | ?.sortedBy { App.context.FAV?.viewed?.indexOf(it.id) } 28 | ?.let { lst.addAll(it) } 29 | } 30 | // Exclude pending 31 | return lst 32 | .filterNot { App.context.viewToRemove.contains(it.id.toString()) } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/helpers/AppVersion.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.helpers 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import androidx.core.content.pm.PackageInfoCompat 7 | 8 | data class AppVersion( 9 | val versionName: String, 10 | val versionNumber: Long, 11 | ) 12 | 13 | fun getAppVersion( 14 | context: Context, 15 | packageName: String = context.packageName 16 | ): AppVersion? { 17 | return try { 18 | val packageManager = context.packageManager 19 | val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 20 | packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) 21 | } else { 22 | packageManager.getPackageInfo(packageName, 0) 23 | } 24 | AppVersion( 25 | versionName = packageInfo.versionName, 26 | versionNumber = PackageInfoCompat.getLongVersionCode(packageInfo), 27 | ) 28 | } catch (_: Exception) { 29 | null 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/helpers/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.helpers 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.cancelChildren 8 | import kotlinx.coroutines.joinAll 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.runBlocking 11 | 12 | object Coroutines { 13 | private val scopeList = hashMapOf>() 14 | private val jobList = hashMapOf>() 15 | 16 | private fun getScope(name: String, dispatcher: CoroutineDispatcher = Dispatchers.IO): CoroutineScope { 17 | synchronized(scopeList) { 18 | val scope = scopeList.get(name) 19 | scope?.let { return it.second } 20 | } 21 | 22 | val job = Job() 23 | val sc = CoroutineScope(dispatcher + job) 24 | synchronized(scopeList) { scopeList[name] = Pair(job, sc) } 25 | jobList[name] = mutableListOf() 26 | return sc 27 | } 28 | 29 | fun running(name: String): Boolean { 30 | //Log.d("*****", "joblist[$name]: ${jobList[name]}") 31 | var active = false 32 | val jobList = jobList[name] 33 | jobList?.forEach { job -> 34 | active = job.isActive 35 | } 36 | return active 37 | } 38 | 39 | fun launch(name: String, fn: suspend () -> Unit) { 40 | synchronized(scopeList) { 41 | val scope = getScope(name) 42 | val jb = scope.launch { fn() } 43 | jobList[name]?.add(jb) 44 | } 45 | } 46 | 47 | fun remove(name: String) { 48 | cancel(name) 49 | synchronized(scopeList) { 50 | scopeList.remove(name) 51 | } 52 | } 53 | 54 | fun cancel(name: String) { 55 | synchronized(scopeList) { 56 | val scope = scopeList.get(name) 57 | scope?.let { 58 | it.first.cancelChildren() 59 | jobList[name]?.clear() 60 | } 61 | } 62 | } 63 | 64 | fun join(name: String) { 65 | runBlocking { 66 | //TODO may be crash 67 | jobList[name]?.joinAll() 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/helpers/PermHelpers.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.helpers 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import android.os.Build.VERSION 9 | import androidx.core.app.ActivityCompat 10 | import top.rootu.lampa.helpers.Helpers.isGenymotion 11 | 12 | object PermHelpers { 13 | // Storage Permissions 14 | const val REQUEST_EXTERNAL_STORAGE: Int = 112 15 | private val PERMISSIONS_STORAGE = arrayOf( 16 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 17 | Manifest.permission.READ_EXTERNAL_STORAGE 18 | ) 19 | 20 | // Mic Permissions 21 | const val REQUEST_MIC: Int = 113 22 | private val PERMISSIONS_MIC = arrayOf( 23 | Manifest.permission.RECORD_AUDIO 24 | ) 25 | 26 | /** 27 | * Checks if the app has permission to write to device storage

28 | * If the app does not has permission then the user will be prompted to grant permissions

29 | * Required for the [Context.getExternalCacheDir]

30 | * NOTE: runs async

31 | * 32 | * @param context to apply permissions to 33 | */ 34 | @JvmStatic 35 | fun verifyStoragePermissions(context: Context?) { 36 | requestPermissions(context, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE) 37 | } 38 | 39 | fun verifyMicPermissions(context: Context?) { 40 | requestPermissions(context, PERMISSIONS_MIC, REQUEST_MIC) 41 | } 42 | 43 | /** 44 | * Only check. There is no prompt. 45 | * 46 | * @param context to apply permissions to 47 | * @return whether permission already granted 48 | */ 49 | @JvmStatic 50 | fun hasStoragePermissions(context: Context?): Boolean { 51 | // Check if we have write permission 52 | return hasPermissions(context, *PERMISSIONS_STORAGE) 53 | } 54 | 55 | fun hasMicPermissions(context: Context?): Boolean { 56 | // Check if we have mic permission 57 | return hasPermissions(context, *PERMISSIONS_MIC) 58 | } 59 | 60 | // Utils 61 | /** 62 | * Shows permissions dialog

63 | * NOTE: runs async 64 | */ 65 | private fun requestPermissions(activity: Context?, permissions: Array, requestId: Int) { 66 | if (!hasPermissions(activity, *permissions) && !isGenymotion) { 67 | if (activity is Activity) { 68 | // We don't have permission so prompt the user 69 | ActivityCompat.requestPermissions( 70 | activity, 71 | permissions, 72 | requestId 73 | ) 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Only check. There is no prompt. 80 | * 81 | * @param context to apply permissions to 82 | * @return whether permission already granted 83 | */ 84 | private fun hasPermissions(context: Context?, vararg permissions: String): Boolean { 85 | if (context == null) { 86 | return false 87 | } 88 | 89 | if (VERSION.SDK_INT >= 23) { 90 | for (permission in permissions) { 91 | val result = ActivityCompat.checkSelfPermission(context, permission) 92 | if (result != PackageManager.PERMISSION_GRANTED) { 93 | return false 94 | } 95 | } 96 | } 97 | 98 | return true 99 | } 100 | 101 | fun hasPermission(context: Context?, permission: String): Boolean { 102 | if (context == null) { 103 | return false 104 | } 105 | 106 | return PackageManager.PERMISSION_GRANTED == context.packageManager.checkPermission( 107 | permission, context.packageName 108 | ) 109 | } 110 | 111 | fun isInstallPermissionDeclared(context: Context): Boolean { 112 | return try { 113 | val packageInfo = context.packageManager.getPackageInfo( 114 | context.packageName, 115 | PackageManager.GET_PERMISSIONS 116 | ) 117 | packageInfo.requestedPermissions?.any { it == "android.permission.REQUEST_INSTALL_PACKAGES" } == true 118 | } catch (_: Exception) { 119 | false 120 | } 121 | } 122 | 123 | fun canRequestPackageInstalls(context: Context): Boolean { 124 | return if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { 125 | context.packageManager.canRequestPackageInstalls() 126 | } else { 127 | // Permission not required before Android 8.0 128 | true 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/models/Releases.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.models 2 | 3 | class Releases : ArrayList() 4 | 5 | data class Release( 6 | val url: String, 7 | val assets_url: String, 8 | val upload_url: String, 9 | val html_url: String, 10 | val id: Int, 11 | val author: Any?, 12 | val node_id: String, 13 | val tag_name: String, 14 | val target_commitish: String, 15 | val name: String, 16 | val draft: Boolean, 17 | val prerelease: Boolean, 18 | val created_at: String, 19 | val published_at: String, 20 | val assets: ArrayList, 21 | val tarball_url: String, 22 | val zipball_url: String, 23 | val body: String, 24 | ) 25 | 26 | data class Asset( 27 | val url: String, 28 | val id: Int, 29 | val node_id: String, 30 | val name: String, 31 | val label: String?, 32 | val uploader: Any?, 33 | val content_type: String, 34 | val state: String, 35 | val size: Int, 36 | val download_count: Int, 37 | val created_at: String, 38 | val updated_at: String, 39 | val browser_download_url: String, 40 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/net/IgnoreSSLTrustManager.java: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.net; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import java.security.cert.X509Certificate; 6 | 7 | import javax.net.ssl.X509TrustManager; 8 | 9 | @SuppressLint("CustomX509TrustManager") 10 | public class IgnoreSSLTrustManager implements X509TrustManager { 11 | 12 | @SuppressLint("TrustAllX509TrustManager") 13 | @Override 14 | public void checkClientTrusted(X509Certificate[] chain, String authType) { 15 | // Perform no check whatsoever on the validity of the SSL certificate 16 | } 17 | 18 | @SuppressLint("TrustAllX509TrustManager") 19 | @Override 20 | public void checkServerTrusted(X509Certificate[] chain, String authType) { 21 | // Perform no check whatsoever on the validity of the SSL certificate 22 | } 23 | 24 | @Override 25 | public X509Certificate[] getAcceptedIssuers() { 26 | return new java.security.cert.X509Certificate[]{}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/net/SelfSignedTrustManager.java: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.net; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.cert.CertificateEncodingException; 8 | import java.security.cert.CertificateException; 9 | import java.security.cert.X509Certificate; 10 | 11 | import javax.net.ssl.X509TrustManager; 12 | 13 | @SuppressLint("CustomX509TrustManager") 14 | public class SelfSignedTrustManager implements X509TrustManager { 15 | 16 | private static final X509Certificate[] acceptedIssuers = new X509Certificate[]{}; 17 | 18 | private final String certKey; 19 | 20 | public SelfSignedTrustManager(String certKey) { 21 | super(); 22 | this.certKey = certKey; 23 | } 24 | 25 | // Thank you: http://stackoverflow.com/questions/1270703/how-to-retrieve-compute-an-x509-certificates-thumbprint-in-java 26 | private static String getThumbPrint(X509Certificate cert) 27 | throws NoSuchAlgorithmException, CertificateEncodingException { 28 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 29 | byte[] der = cert.getEncoded(); 30 | md.update(der); 31 | byte[] digest = md.digest(); 32 | return hexify(digest); 33 | } 34 | 35 | private static String hexify(byte[] bytes) { 36 | 37 | char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 38 | StringBuilder buf = new StringBuilder(bytes.length * 2); 39 | for (byte aByte : bytes) { 40 | buf.append(hexDigits[(aByte & 0xf0) >> 4]); 41 | buf.append(hexDigits[aByte & 0x0f]); 42 | } 43 | return buf.toString(); 44 | 45 | } 46 | 47 | @SuppressLint("TrustAllX509TrustManager") 48 | @Override 49 | public void checkClientTrusted(X509Certificate[] chain, String authType) { 50 | } 51 | 52 | @Override 53 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 54 | if (this.certKey == null) { 55 | throw new CertificateException("Requires a non-null certificate key in SHA-1 format to match."); 56 | } 57 | 58 | // Qe have a certKey defined. We should now examine the one we got from the server. 59 | // They match? All is good. They don't, throw an exception. 60 | String ourKey = this.certKey.replaceAll("[^a-fA-F0-9]+", ""); 61 | try { 62 | // Assume self-signed root is okay? 63 | X509Certificate sslCert = chain[0]; 64 | String thumbprint = SelfSignedTrustManager.getThumbPrint(sslCert); 65 | if (ourKey.equalsIgnoreCase(thumbprint)) { 66 | return; 67 | } 68 | 69 | //Log.e(SelfSignedTrustManager.class.getSimpleName(), certificateException.toString()); 70 | throw new CertificateException("Certificate key [" + thumbprint + "] doesn't match expected value."); 71 | 72 | } catch (NoSuchAlgorithmException e) { 73 | throw new CertificateException("Unable to check self-signed cert, unknown algorithm. " + e); 74 | } 75 | 76 | } 77 | 78 | @Override 79 | public X509Certificate[] getAcceptedIssuers() { 80 | return acceptedIssuers; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/net/TlsSocketFactory.java: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.net; 2 | 3 | import org.conscrypt.Conscrypt; 4 | 5 | import java.io.IOException; 6 | import java.net.InetAddress; 7 | import java.net.Socket; 8 | import java.security.KeyManagementException; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.Provider; 11 | import java.security.Security; 12 | 13 | import javax.net.ssl.SSLContext; 14 | import javax.net.ssl.SSLSocket; 15 | import javax.net.ssl.SSLSocketFactory; 16 | import javax.net.ssl.TrustManager; 17 | import javax.net.ssl.X509TrustManager; 18 | 19 | public class TlsSocketFactory extends SSLSocketFactory { 20 | private static Provider conscrypt; 21 | public static final String[] TLS_MODERN = {"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"}; 22 | public static final String[] TLS_RESTRICTED = {"TLSv1.2", "TLSv1.3"}; 23 | private final String[] enabledProtocols; 24 | private final SSLSocketFactory delegate; 25 | public static final X509TrustManager trustAllCerts = new IgnoreSSLTrustManager(); 26 | 27 | public TlsSocketFactory(String[] enabledProtocols) throws KeyManagementException, NoSuchAlgorithmException { 28 | this.enabledProtocols = enabledProtocols; 29 | this.delegate = getSocketFactory(); 30 | } 31 | 32 | public TlsSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { 33 | this.enabledProtocols = TLS_RESTRICTED; 34 | this.delegate = getSocketFactory(); 35 | } 36 | 37 | public TlsSocketFactory(SSLSocketFactory base) { 38 | this.enabledProtocols = TLS_RESTRICTED; 39 | this.delegate = base; 40 | } 41 | 42 | private static SSLSocketFactory getSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { 43 | if (TlsSocketFactory.conscrypt == null) { 44 | TlsSocketFactory.conscrypt = Conscrypt.newProvider(); 45 | // Add as provider 46 | Security.insertProviderAt(conscrypt, 1); 47 | } 48 | SSLContext context = SSLContext.getInstance("TLS", TlsSocketFactory.conscrypt); 49 | context.init(null, new TrustManager[]{trustAllCerts}, null); 50 | return context.getSocketFactory(); 51 | } 52 | 53 | @Override 54 | public String[] getDefaultCipherSuites() { 55 | return delegate.getDefaultCipherSuites(); 56 | } 57 | 58 | @Override 59 | public String[] getSupportedCipherSuites() { 60 | return delegate.getSupportedCipherSuites(); 61 | } 62 | 63 | @Override 64 | public Socket createSocket() throws IOException { 65 | return patch(delegate.createSocket()); 66 | } 67 | 68 | @Override 69 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 70 | return patch(delegate.createSocket(s, host, port, autoClose)); 71 | } 72 | 73 | @Override 74 | public Socket createSocket(String host, int port) throws IOException { 75 | return patch(delegate.createSocket(host, port)); 76 | } 77 | 78 | @Override 79 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { 80 | return patch(delegate.createSocket(host, port, localHost, localPort)); 81 | } 82 | 83 | @Override 84 | public Socket createSocket(InetAddress host, int port) throws IOException { 85 | return patch(delegate.createSocket(host, port)); 86 | } 87 | 88 | @Override 89 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { 90 | return patch(delegate.createSocket(address, port, localAddress, localPort)); 91 | } 92 | 93 | private Socket patch(Socket s) { 94 | if (s instanceof SSLSocket) { 95 | ((SSLSocket) s).setEnabledProtocols(enabledProtocols); 96 | } 97 | return s; 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/receivers/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.receivers 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import top.rootu.lampa.BuildConfig 8 | import top.rootu.lampa.sched.Scheduler 9 | 10 | // https://stackoverflow.com/questions/46445265/android-8-0-java-lang-illegalstateexception-not-allowed-to-start-service-inten/49846410#49846410 11 | class BootReceiver : BroadcastReceiver() { 12 | override fun onReceive(context: Context?, intent: Intent?) { 13 | if (Intent.ACTION_BOOT_COMPLETED == intent!!.action) { 14 | if (BuildConfig.DEBUG) 15 | Log.d("*****", "onReceive: BOOT_COMPLETED") 16 | Scheduler.scheduleUpdate(true) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/receivers/HomeWatch.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.receivers 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.media.tv.TvContract 7 | import android.os.Build 8 | import android.util.Log 9 | import androidx.annotation.RequiresApi 10 | import androidx.tvprovider.media.tv.TvContractCompat 11 | import top.rootu.lampa.App 12 | import top.rootu.lampa.BuildConfig 13 | import top.rootu.lampa.channels.ChannelManager 14 | import top.rootu.lampa.channels.WatchNext 15 | import top.rootu.lampa.helpers.ChannelHelper 16 | import top.rootu.lampa.helpers.Helpers 17 | import top.rootu.lampa.helpers.Helpers.isTvContentProviderAvailable 18 | import top.rootu.lampa.helpers.Prefs.isInWatchNext 19 | import top.rootu.lampa.sched.Scheduler 20 | 21 | private const val TAG: String = "HomeWatch" 22 | 23 | @RequiresApi(Build.VERSION_CODES.O) 24 | class HomeWatch() : BroadcastReceiver() { 25 | 26 | override fun onReceive(context: Context, intent: Intent) { 27 | 28 | val action = intent.action 29 | 30 | if (action == null || !(isTvContentProviderAvailable)) 31 | return 32 | 33 | val watchNextId = intent.getLongExtra(TvContract.EXTRA_WATCH_NEXT_PROGRAM_ID, -1L) 34 | val previewId = intent.getLongExtra(TvContract.EXTRA_PREVIEW_PROGRAM_ID, -1L) 35 | 36 | when (action) { 37 | 38 | TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> { 39 | if (BuildConfig.DEBUG) 40 | Log.d(TAG, "ACTION_INITIALIZE_PROGRAMS received") 41 | Scheduler.scheduleUpdate(true) 42 | } 43 | 44 | TvContractCompat.ACTION_PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT -> { 45 | 46 | val movieId = WatchNext.getInternalIdFromWatchNextProgramId(watchNextId) 47 | if (BuildConfig.DEBUG) 48 | Log.d( 49 | TAG, 50 | "ACTION_PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT, preview $previewId, watch-next $watchNextId movieId $movieId" 51 | ) 52 | movieId?.let { 53 | if (BuildConfig.DEBUG) Log.d( 54 | TAG, 55 | "$it isInWatchNext? ${App.context.isInWatchNext(it)} card ${ 56 | WatchNext.getCardFromWatchNextProgramId(watchNextId) 57 | }" 58 | ) 59 | if (!App.context.isInWatchNext(it)) { 60 | val card = WatchNext.getCardFromWatchNextProgramId(watchNextId) 61 | Helpers.manageFavorite("add", "wath", it, card) 62 | } 63 | } 64 | } 65 | 66 | TvContractCompat.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED -> { 67 | 68 | val movieId = WatchNext.getInternalIdFromWatchNextProgramId(watchNextId) 69 | if (BuildConfig.DEBUG) 70 | Log.d( 71 | TAG, 72 | "ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED, watch-next $watchNextId movieId $movieId" 73 | ) 74 | movieId?.let { 75 | if (BuildConfig.DEBUG) Log.d( 76 | TAG, 77 | "$it isInWatchNext? ${App.context.isInWatchNext(it)} card ${ 78 | WatchNext.getCardFromWatchNextProgramId(watchNextId) 79 | }" 80 | ) 81 | if (App.context.isInWatchNext(movieId)) { 82 | Helpers.manageFavorite("rem", "wath", movieId) 83 | } 84 | try { // remove from contentPrivider 85 | WatchNext.rem(movieId) 86 | } catch (e: Exception) { 87 | Log.e(TAG, "error delete $movieId from WatchNext: $e") 88 | } 89 | } 90 | } 91 | 92 | TvContractCompat.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED -> { 93 | if (BuildConfig.DEBUG) 94 | Log.d(TAG, "ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED, preview $previewId") 95 | val movieIdAndChanId = 96 | ChannelManager.getInternalIdAndChanIdFromPreviewProgramId(previewId) 97 | movieIdAndChanId.first?.let { movieId -> 98 | val chan = movieIdAndChanId.second?.let { ChannelHelper.getChanByID(it) } 99 | if (!chan.isNullOrEmpty()) 100 | Helpers.manageFavorite("rem", chan, movieId) 101 | try { // remove from contentPrivider 102 | movieIdAndChanId.second?.let { chid -> 103 | ChannelManager.deleteFromChannel( 104 | chid, 105 | movieId 106 | ) 107 | } 108 | } catch (e: Exception) { 109 | Log.e(TAG, "error delete $movieId from channel $chan: $e") 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/sched/ContentAlarmManager.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.sched 2 | 3 | import android.app.IntentService 4 | import android.content.Intent 5 | import android.os.Build 6 | import androidx.annotation.RequiresApi 7 | import top.rootu.lampa.recs.RecsService 8 | 9 | @Suppress("DEPRECATION") 10 | @Deprecated("IntentService is deprecated.") 11 | class ContentAlarmManager : IntentService("ContentAlarmManager") { 12 | 13 | @RequiresApi(Build.VERSION_CODES.KITKAT) 14 | @Suppress("DEPRECATION") 15 | @Deprecated("Migrate to WorkManager's doWork()") 16 | override fun onHandleIntent(intent: Intent?) { 17 | RecsService.updateRecs() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/sched/ContentJobService.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.sched 2 | 3 | import android.app.job.JobParameters 4 | import android.app.job.JobService 5 | import android.os.Build 6 | import android.util.Log 7 | import androidx.annotation.RequiresApi 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.cancel 11 | import kotlinx.coroutines.launch 12 | import top.rootu.lampa.BuildConfig 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | 15 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 16 | class ContentJobService : JobService() { 17 | private val isJobComplete = AtomicBoolean(false) 18 | private val jobScope = CoroutineScope(Dispatchers.IO) 19 | 20 | override fun onStartJob(params: JobParameters?): Boolean { 21 | isJobComplete.set(false) 22 | 23 | jobScope.launch { 24 | try { 25 | if (BuildConfig.DEBUG) Log.i( 26 | "ContentJobService", 27 | "ContentJobService call updateContent(sync = true)" 28 | ) 29 | Scheduler.updateContent(sync = true) // Update content with sync enabled 30 | } catch (e: Exception) { 31 | if (BuildConfig.DEBUG) Log.e("ContentJobService", "Error updating content", e) 32 | } finally { 33 | isJobComplete.set(true) 34 | jobFinished(params, false) // Notify the system that the job is complete 35 | } 36 | } 37 | 38 | // Return true to indicate that the job is running on a separate thread 39 | return true 40 | } 41 | 42 | override fun onStopJob(params: JobParameters?): Boolean { 43 | // Return true to reschedule the job if it was stopped prematurely 44 | return !isJobComplete.get() 45 | } 46 | 47 | override fun onDestroy() { 48 | super.onDestroy() 49 | jobScope.cancel() // Cancel the coroutine scope when the service is destroyed 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/search/SearchCommand.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.search 2 | 3 | import android.database.Cursor 4 | import android.database.MatrixCursor 5 | import android.util.Log 6 | import top.rootu.lampa.App 7 | import top.rootu.lampa.BuildConfig 8 | import top.rootu.lampa.R 9 | import top.rootu.lampa.helpers.Helpers.openLampa 10 | import top.rootu.lampa.helpers.Helpers.openSettings 11 | import top.rootu.lampa.helpers.Helpers.uninstallSelf 12 | import top.rootu.lampa.search.SearchProvider.Companion.queryProjection 13 | import java.util.Locale 14 | 15 | object SearchCommand { 16 | fun exec(query: String): Cursor? { 17 | if (BuildConfig.DEBUG) Log.d("*****", "SearchCommand exec($query)") 18 | // just fun 19 | if (query.lowercase(Locale.getDefault()) 20 | .contains(App.context.getString(R.string.open_lampa)) 21 | ) { 22 | if (BuildConfig.DEBUG) Log.d("*****", "SearchCommand matched - openLampa()") 23 | openLampa() 24 | return MatrixCursor(queryProjection, 0) 25 | } 26 | if (query.lowercase(Locale.getDefault()) 27 | .contains(App.context.getString(R.string.open_settings)) 28 | ) { 29 | if (BuildConfig.DEBUG) Log.d("*****", "SearchCommand matched - openSettings()") 30 | openSettings() 31 | return MatrixCursor(queryProjection, 0) 32 | } 33 | if (query.lowercase(Locale.getDefault()) 34 | .contains(App.context.getString(R.string.lampa_suxx)) 35 | ) { 36 | uninstallSelf() 37 | return MatrixCursor(queryProjection, 0) 38 | } 39 | return null 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/search/SearchLocal.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.search 2 | 3 | class SearchLocal { 4 | // TODO 5 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/search/SearchProvider.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.search 2 | 3 | import android.app.SearchManager 4 | import android.content.ContentProvider 5 | import android.content.ContentValues 6 | import android.content.UriMatcher 7 | import android.database.Cursor 8 | import android.database.MatrixCursor 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.provider.BaseColumns 12 | import android.util.Log 13 | import androidx.annotation.RequiresApi 14 | import java.io.IOException 15 | 16 | class SearchProvider : ContentProvider() { 17 | private val mUriMatcher: UriMatcher = buildUriMatcher() 18 | 19 | override fun onCreate(): Boolean { 20 | return true 21 | } 22 | 23 | @RequiresApi(Build.VERSION_CODES.KITKAT) 24 | override fun query( 25 | uri: Uri, 26 | projection: Array?, 27 | selection: String?, 28 | selectionArgs: Array?, 29 | sortOrder: String? 30 | ): Cursor { 31 | if (mUriMatcher.match(uri) == SEARCH_SUGGEST) { 32 | var rawQuery = "" 33 | if (!selectionArgs.isNullOrEmpty()) { 34 | rawQuery = selectionArgs[0] 35 | } 36 | 37 | if (rawQuery.isEmpty()) 38 | uri.lastPathSegment?.let { 39 | rawQuery = it 40 | } 41 | 42 | if (rawQuery.isEmpty()) 43 | return MatrixCursor(queryProjection, 0) 44 | 45 | if (rawQuery == "dummy") 46 | return MatrixCursor(queryProjection, 0) 47 | 48 | val ret = SearchCommand.exec(rawQuery) 49 | ret?.let { 50 | return ret 51 | } 52 | 53 | return search(rawQuery) 54 | } else { 55 | Log.i("*****", "Unknown Uri: $uri") 56 | return MatrixCursor(queryProjection, 0) 57 | } 58 | } 59 | 60 | @RequiresApi(Build.VERSION_CODES.KITKAT) 61 | private fun search(query: String?): Cursor { 62 | val matrixCursor = MatrixCursor(queryProjection) 63 | query ?: return matrixCursor 64 | try { 65 | val results = SearchDatabase.search(query) 66 | if (results.isNotEmpty()) { 67 | return SearchDatabase.getMatrix(results) 68 | } 69 | } catch (e: IOException) { 70 | e.printStackTrace() 71 | } 72 | 73 | return matrixCursor 74 | } 75 | 76 | override fun getType(uri: Uri): String? { 77 | return null 78 | } 79 | 80 | override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { 81 | throw UnsupportedOperationException("Insert is not implemented.") 82 | } 83 | 84 | override fun delete(uri: Uri, s: String?, strings: Array?): Int { 85 | throw UnsupportedOperationException("Delete is not implemented.") 86 | } 87 | 88 | override fun update( 89 | uri: Uri, 90 | contentValues: ContentValues?, 91 | s: String?, 92 | strings: Array? 93 | ): Int { 94 | throw UnsupportedOperationException("Update is not implemented.") 95 | } 96 | 97 | companion object { 98 | private const val AUTHORITY = "top.rootu.lampa.atvsearch" 99 | private const val SEARCH_SUGGEST = 1 100 | 101 | val queryProjection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 102 | arrayOf( 103 | BaseColumns._ID, 104 | SearchDatabase.KEY_NAME, 105 | SearchDatabase.KEY_DESCRIPTION, 106 | SearchDatabase.KEY_ICON, 107 | SearchDatabase.KEY_DATA_TYPE, 108 | SearchDatabase.KEY_IS_LIVE, 109 | SearchDatabase.KEY_VIDEO_WIDTH, 110 | SearchDatabase.KEY_VIDEO_HEIGHT, 111 | SearchDatabase.KEY_AUDIO_CHANNEL_CONFIG, 112 | SearchDatabase.KEY_PURCHASE_PRICE, 113 | SearchDatabase.KEY_RENTAL_PRICE, 114 | SearchDatabase.KEY_RATING_STYLE, 115 | SearchDatabase.KEY_RATING_SCORE, 116 | SearchDatabase.KEY_PRODUCTION_YEAR, 117 | SearchDatabase.KEY_COLUMN_DURATION, 118 | SearchDatabase.KEY_ACTION, 119 | SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, 120 | SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA 121 | ) else 122 | arrayOf( 123 | BaseColumns._ID, 124 | SearchDatabase.KEY_NAME, 125 | SearchDatabase.KEY_DESCRIPTION, 126 | SearchDatabase.KEY_ICON, 127 | SearchDatabase.KEY_ACTION, 128 | SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, 129 | SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA 130 | ) 131 | 132 | private fun buildUriMatcher(): UriMatcher { 133 | val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) 134 | uriMatcher.addURI( 135 | AUTHORITY, "/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST 136 | ) 137 | uriMatcher.addURI( 138 | AUTHORITY, 139 | "/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 140 | SEARCH_SUGGEST 141 | ) 142 | return uriMatcher 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/search/Utils.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.search 2 | 3 | import top.rootu.lampa.tmdb.models.entity.Entity 4 | import java.util.* 5 | import kotlin.math.min 6 | 7 | object Utils { 8 | fun getDistance(ent: Entity, query: String): Int { 9 | var lvt = 100000 10 | var lvo = 100000 11 | ent.title?.let { 12 | lvt = levenstain(it.lowercase(Locale.getDefault()), query.lowercase(Locale.getDefault())) 13 | } 14 | ent.original_title?.let { 15 | lvo = levenstain(it.lowercase(Locale.getDefault()), query.lowercase(Locale.getDefault())) 16 | } 17 | return min(lvt, lvo) 18 | } 19 | 20 | fun levenstain(str1: String, str2: String): Int { 21 | val Di_1 = IntArray(str2.length + 1) 22 | val Di = IntArray(str2.length + 1) 23 | for (j in 0..str2.length) { 24 | Di[j] = j // (i == 0) 25 | } 26 | for (i in 1..str1.length) { 27 | System.arraycopy(Di, 0, Di_1, 0, Di_1.size) 28 | Di[0] = i // (j == 0) 29 | for (j in 1..str2.length) { 30 | val cost = if (str1[i - 1] != str2[j - 1]) 1 else 0 31 | Di[j] = min( 32 | Di_1[j] + 1, 33 | Di[j - 1] + 1, 34 | Di_1[j - 1] + cost 35 | ) 36 | } 37 | } 38 | return Di[Di.size - 1] 39 | } 40 | 41 | private fun min(n1: Int, n2: Int, n3: Int): Int { 42 | return min(min(n1, n2), n3) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/Images.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb 2 | 3 | import android.net.Uri 4 | import android.os.Build 5 | import androidx.core.net.toUri 6 | import okhttp3.Request 7 | import top.rootu.lampa.App 8 | import top.rootu.lampa.helpers.Helpers.getJson 9 | import top.rootu.lampa.helpers.Prefs.tmdbApiUrl 10 | import top.rootu.lampa.tmdb.models.entity.Entity 11 | import top.rootu.lampa.tmdb.models.entity.Images 12 | import java.io.IOException 13 | 14 | object Images { 15 | fun get(entity: Entity) { 16 | if (entity.images != null) 17 | return 18 | 19 | val apiUrl = App.context.tmdbApiUrl 20 | val apiUri = apiUrl.toUri() 21 | // Manually handle the authority part to prevent encoding of the port colon 22 | val authority = "${apiUri.host}${if (apiUri.port != -1) ":${apiUri.port}" else ""}" 23 | val basePath = apiUri.path?.removeSuffix("/") ?: "3" 24 | val urlBuilder = Uri.Builder() 25 | .scheme(apiUri.scheme) 26 | .encodedAuthority(authority) // Use encodedAuthority instead of authority to prevent double encoding 27 | .path("$basePath/${entity.media_type}/${entity.id}/images") 28 | 29 | val params = mutableMapOf() 30 | // key must be 1st 31 | params["api_key"] = TMDB.APIKEY 32 | params["language"] = TMDB.getLang() 33 | params["include_image_language"] = "${TMDB.getLang()},en,null" 34 | for (param in params) { 35 | urlBuilder.appendQueryParameter(param.key, param.value) 36 | } 37 | // Add all original query parameters 38 | if (apiUrl != TMDB.APIURL) 39 | apiUri.queryParameterNames.forEach { paramName -> 40 | apiUri.getQueryParameter(paramName)?.let { paramValue -> 41 | urlBuilder.appendQueryParameter(paramName, paramValue) 42 | } 43 | } 44 | val link = urlBuilder.build().toString() 45 | 46 | var body: String? = null 47 | try { 48 | val request = Request.Builder() 49 | .url(link) 50 | .build() 51 | val client = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 52 | TMDB.startWithQuad9DNS() else TMDB.permissiveOkHttp() 53 | client.newCall(request).execute().use { response -> 54 | if (!response.isSuccessful) throw IOException("Unexpected code $response") 55 | body = response.body()?.string() 56 | response.body()?.close() 57 | } 58 | } catch (e: Exception) { 59 | e.printStackTrace() 60 | } 61 | 62 | val images = getJson(body, Images::class.java) 63 | images?.let { img -> 64 | for (i in 0 until img.backdrops.size) 65 | img.backdrops[i].file_path = 66 | TMDB.imageUrl(img.backdrops[i].file_path).replace("original", "w1280") 67 | 68 | for (i in 0 until img.posters.size) 69 | img.posters[i].file_path = 70 | TMDB.imageUrl(img.posters[i].file_path).replace("original", "w500") 71 | } 72 | 73 | entity.images = images 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/content_ratings/ContentRatingsModel.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.content_ratings 2 | 3 | data class ContentRatingsModel( 4 | val id: Int, 5 | val results: List? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/content_ratings/Result.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.content_ratings 2 | 3 | data class Result( 4 | val iso_3166_1: String, 5 | val rating: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/BelongsToCollection.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class BelongsToCollection( 4 | val backdrop_path: String, 5 | val id: Int, 6 | val name: String, 7 | val poster_path: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Entities.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | data class Entities( 3 | var page: Int, 4 | var total_results: Int, 5 | var total_pages: Int, 6 | var results: List 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Entity.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | import top.rootu.lampa.tmdb.models.titles.AlternativeTitles 4 | import top.rootu.lampa.tmdb.models.trailers.Trailers 5 | 6 | data class Entity( 7 | var adult: Boolean?, 8 | var alternative_titles: AlternativeTitles?, 9 | var backdrop_path: String?, 10 | var belongs_to_collection: BelongsToCollection?, 11 | var budget: Int?, 12 | var certification: String?, 13 | var character: String?, 14 | var credit_id: String?, 15 | var episode_run_time: List?, 16 | var first_air_date: String?, 17 | var genre_ids: List?, 18 | var genres: List?, 19 | var homepage: String?, 20 | var id: Int?, 21 | var images: Images?, 22 | var imdb_id: String?, 23 | var in_production: Boolean?, 24 | var languages: Any?, 25 | var last_air_date: String?, 26 | var media_type: String?, 27 | var name: String?, 28 | var networks: List?, 29 | var number_of_episodes: Int?, 30 | var number_of_seasons: Int?, 31 | var origin_country: List?, 32 | var original_language: String?, 33 | var original_name: String?, 34 | var original_title: String?, 35 | var overview: String?, 36 | var popularity: Double?, 37 | var poster_path: String?, 38 | var production_companies: List?, 39 | var production_countries: List?, 40 | var release_date: String?, 41 | var revenue: Long?, 42 | var runtime: Int?, 43 | var seasons: List?, 44 | var spoken_languages: List?, 45 | var status: String?, 46 | var tagline: String?, 47 | var title: String?, 48 | var type: String?, 49 | var video: Boolean?, 50 | var vote_average: Double?, 51 | var vote_count: Int?, 52 | var year: String?, 53 | var videos: Trailers?, 54 | ) { 55 | override fun toString(): String = 56 | (id?.toString() ?: "-1") + ": " + (title ?: "") + " " + (original_title ?: "") + " " + (year 57 | ?: "") 58 | } -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Genre.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class Genre( 4 | val id: Int, 5 | val name: String?, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Image.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class Image( 4 | var aspect_ratio: Double, 5 | var file_path: String, 6 | var height: Int, 7 | var iso_639_1: String, 8 | var vote_average: Double, 9 | var vote_count: Int, 10 | var width: Int 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Images.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class Images( 4 | val backdrops: List, 5 | val posters: List 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/ProductionCompany.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class ProductionCompany( 4 | val id: Int, 5 | var logo_path: String, 6 | val name: String, 7 | val origin_country: String 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/ProductionCountry.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class ProductionCountry( 4 | val iso_3166_1: String, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/Season.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class Season( 4 | var air_date: String?, 5 | val episode_count: Int, 6 | val id: Int, 7 | val name: String?, 8 | val overview: String?, 9 | var poster_path: String?, 10 | val season_number: Int 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/entity/SpokenLanguage.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.entity 2 | 3 | data class SpokenLanguage( 4 | val iso_639_1: String, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/release_dates/ReleaseDate.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.release_dates 2 | 3 | data class ReleaseDate( 4 | val certification: String, 5 | val iso_639_1: String, 6 | val note: String, 7 | val release_date: String, 8 | val type: Int 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/release_dates/ReleaseDatesModel.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.release_dates 2 | data class ReleaseDatesModel( 3 | val id: Int, 4 | val results: List 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/release_dates/Result.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.release_dates 2 | data class Result( 3 | val iso_3166_1: String, 4 | val release_dates: List 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/titles/AlternativeTitles.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.titles 2 | 3 | data class AlternativeTitles( 4 | val id: Int?, // optional 5 | val titles: List?, // movie titles 6 | val results: List? // tv titles 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/titles/Result.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.titles 2 | 3 | data class Result( 4 | val iso_3166_1: String, 5 | val title: String, 6 | val type: String? 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/trailers/Trailer.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.trailers 2 | 3 | data class Trailer( 4 | var id: String?, 5 | var iso_3166_1: String?, 6 | var iso_639_1: String?, 7 | var key: String?, 8 | var name: String?, 9 | var site: String?, 10 | var size: Int?, 11 | var type: String?, 12 | 13 | var poster: String?, 14 | var link: String? 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/rootu/lampa/tmdb/models/trailers/Trailers.kt: -------------------------------------------------------------------------------- 1 | package top.rootu.lampa.tmdb.models.trailers 2 | 3 | data class Trailers( 4 | val id: Int, 5 | val results: List 6 | ) -------------------------------------------------------------------------------- /app/src/main/res/color/action_mic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/empty_poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xhdpi/empty_poster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/lampa_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xhdpi/lampa_banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/lampa_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xhdpi/lampa_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/lampa_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 12 | 13 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/lampa_logo_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xhdpi/lampa_logo_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/lampa_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xxxhdpi/lampa_banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/lampa_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable-xxxhdpi/lampa_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/active_menu_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ch_book_shape.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ch_hist_shape.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ch_like_shape.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dialog_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dropdown_bg_focused.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dropdown_bg_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dropdown_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dropdown_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lampa_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/app/src/main/res/drawable/lampa_banner.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/lampa_logo_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_close_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_content_copy_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_exit_to_app_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_explorer_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_link_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_save_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_settings_backup_restore_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_orb_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/welcome.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 32 | 33 | 45 | 46 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_xwalk.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 32 | 33 | 45 | 46 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 27 | 28 | 34 | 35 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_list_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_input_url.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 33 | 34 | 38 | 39 | 40 | 41 | 48 | 49 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/layout/lampa_dropdown_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loader.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/raw/cub_watch: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB 3 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 4 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 5 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx 6 | MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV 7 | BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE 8 | ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g 9 | VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC 10 | AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N 11 | TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj 12 | eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E 13 | oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk 14 | Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY 15 | uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j 16 | BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb 17 | +ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G 18 | A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw 19 | CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0 20 | LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr 21 | BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv 22 | bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov 23 | L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H 24 | ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH 25 | 7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi 26 | H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx 27 | RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv 28 | xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38 29 | sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL 30 | l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq 31 | 6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY 32 | LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5 33 | yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K 34 | 00u/I5sUKUErmgQfky3xxzlIPK1aEn8= 35 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /app/src/main/res/raw/cub_watch2: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB 3 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 4 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 5 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw 6 | MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV 7 | BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU 8 | aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy 9 | dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 10 | AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B 11 | 3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY 12 | tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ 13 | Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 14 | VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT 15 | 79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 16 | c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT 17 | Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l 18 | c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee 19 | UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE 20 | Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd 21 | BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G 22 | A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF 23 | Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO 24 | VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 25 | ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs 26 | 8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR 27 | iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze 28 | Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ 29 | XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ 30 | qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB 31 | VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB 32 | L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG 33 | jjxDah2nGN59PRbxYvnKkKj9 34 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /app/src/main/res/raw/cub_watch3: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGKzCCBROgAwIBAgIRAPmOtu8AygTDRdygyE9IpDwwDQYJKoZIhvcNAQELBQAw 3 | gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO 4 | BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE 5 | AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD 6 | QTAeFw0yMTExMjIwMDAwMDBaFw0yMjExMjIyMzU5NTlaMBYxFDASBgNVBAMMCyou 7 | Y3ViLndhdGNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvuEpj3o6 8 | nhQtnMpapW0HS5UKgDR4ICuzCRdH+CNveUDLrmYJlONUaLeUAd5jhXkSmH86C8fY 9 | zvdU3C+nI7KEfYvWi0EYTSTAhkT450DI9dvWg1p+8p8yz6qlbkthSg+r3lqiQ2ZP 10 | QTHEOdl3Hch1LqhUIoQw9DCPxOg28DhUMrtPMNbDDGTTY/aTYd8sgTMxTA2TYzmB 11 | /aj7gcWL7ctB/mkAW57p944mVScWR6V2qU0OmSjAtlFtr+gJKA4d9/kAZDsBlxb8 12 | Cwe31cU+rXJvleRrw+3QposXPOED6m4YMdQ0ETk/gyD6n9nF2JioEF1sBezwHI9G 13 | 6N47d70YzsamhQIDAQABo4IC+DCCAvQwHwYDVR0jBBgwFoAUjYxexFStiuF36Zv5 14 | mwXhuAGNYeEwHQYDVR0OBBYEFL27RM8HMJcdYINIDNyE6FJdIlNlMA4GA1UdDwEB 15 | /wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF 16 | BQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0 17 | dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2 18 | ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29SU0FE 19 | b21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0MCMGCCsGAQUFBzABhhdo 20 | dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAhBgNVHREEGjAYggsqLmN1Yi53YXRjaIIJ 21 | Y3ViLndhdGNoMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdwBGpVXrdfqRIDC1 22 | oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAX1IBUjOAAAEAwBIMEYCIQD1BigoC9cb 23 | Fxj12mCmHAKKJKa9w7eQJDQFkWCnhTjd4wIhAPMNN6HdUB7LTG1SdQQZYuSz8EcA 24 | kZGWCQD61Ct1QYgjAHYAQcjKsd8iRkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvYA 25 | AAF9SAVIkQAABAMARzBFAiAHTSopMAJNTW2gNir5HbAv7od86wUCPTm++AvITfEk 26 | fQIhAJMpJpv3nFhzYyCFNufKtEupDMwO/h5YMNku4SBmQobcAHUAKXm+8J45OSHw 27 | VnOfY6V35b5XfZxgCvj5TV0mXCVdx4QAAAF9SAVIXwAABAMARjBEAiBWFzG1UYAz 28 | dZGlsdXD1pZs2y32DJUDzE9n+MaTreRbaAIgcXdavNfycEO4/J6QCiqnCDgvaygU 29 | 05Qpw+zxIvaXdYQwDQYJKoZIhvcNAQELBQADggEBAAPPdjlWHzOSuVDyh6z558Zn 30 | yL7dvo0L223SdggDfTZB8yAOSsZtZis7R4dp1yY6vsmG0kq7HjsUMD4ShCuo5h8g 31 | sfW3JzJQ7IfEhR7z9bJ69yBK/TlvzqkeHcZ0qrmrAExm1ddHfTXZJmmYz+dv9urd 32 | z7gR0mjFO44CZwnjGwgDx4X/cZe+wua6PhrpDrlJfYt7d0p7yjpO5MF/UoO0RwhD 33 | Z83biZm2VnWg2ALJ2Cw1iEa/2WgfpP4J/2URlAZ00bT5vJOMZQZSmQcGXHntYykP 34 | WSOMhug9h4+OE/X6/Eey58/os3MYYfPvuECyCNR4LA1cPfEzfup0dCgFFaCCS50= 35 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /app/src/main/res/raw/loader.json: -------------------------------------------------------------------------------- 1 | {"v":"5.5.7","fr":30,"ip":0,"op":40,"w":1920,"h":1080,"nm":"nfocus-loader-white","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"timer","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":23,"s":[360]}],"ix":10},"p":{"a":0,"k":[960,540,0],"ix":2},"a":{"a":0,"k":[0,40,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[13,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":264,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"middle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30,30],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":40,"s":[0]}],"ix":10},"p":{"a":0,"k":[960,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[260,260],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0.9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[100]},{"t":40,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[0]},{"t":40,"s":[99.9]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":25,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[260,260],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1e1f21 4 | #ee1e1f21 5 | #6200ee 6 | #3700b3 7 | #03dac5 8 | #009688 9 | #000 10 | #cc000000 11 | #eee 12 | #1affffff 13 | #5affffff 14 | #ff5251 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20dp 4 | 2.0dip 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 30 | 31 | 38 | 39 | 45 | 46 | 49 | 50 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | cub.watch 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | -------------------------------------------------------------------------------- /app/src/ruStore/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | // https://developer.android.com/build/kotlin-support 4 | // https://developer.android.com/build/releases/gradle-plugin 5 | // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-gradle-plugin 6 | // https://mvnrepository.com/artifact/com.android.tools/r8?repo=google 7 | ext.kp_version = '1.8.22' 8 | ext.gp_version = '7.4.2' 9 | ext.tools_version = '4.0.63' 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { 14 | url = uri("https://storage.googleapis.com/r8-releases/raw") 15 | } 16 | } 17 | dependencies { 18 | classpath "com.android.tools.build:gradle:$gp_version" 19 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kp_version" 20 | //noinspection GradleDependency downgrade to 4.x release - 8.x have verify release issues on api19 21 | classpath("com.android.tools:r8:$tools_version") 22 | } 23 | } 24 | 25 | plugins { 26 | id 'com.google.devtools.ksp' version "${kp_version}-1.0.11" apply false 27 | id 'org.jetbrains.kotlin.android' version "${kp_version}" apply false 28 | } 29 | 30 | tasks.register('clean', Delete) { 31 | delete rootProject.buildDir 32 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.defaults.buildfeatures.buildconfig=true 10 | android.enableJetifier=true 11 | android.nonFinalResIds=false 12 | android.nonTransitiveRClass=true 13 | android.suppressUnsupportedCompileSdk=34 14 | android.useAndroidX=true 15 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Nov 05 22:32:34 MSK 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lampa_banner_xxxhdpi.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampa-app/LAMPA/4734d2ccc2b35dbbf29372b78e34ab2c2c8a14a9/lampa_banner_xxxhdpi.psd -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | maven { 8 | url 'https://download.01.org/crosswalk/releases/crosswalk/android/maven2' 9 | } 10 | } 11 | } 12 | rootProject.name = "LAMPA" 13 | include ':app' 14 | --------------------------------------------------------------------------------