├── .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 |
5 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
--------------------------------------------------------------------------------