├── README.md
├── README_DE.md
├── README_JA.md
└── app
├── .gitignore
├── build.gradle.kts
├── libs
└── KetchumSDK_Community_20240307.jar
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── com
│ └── frank
│ └── glyphify
│ └── ExampleInstrumentedTest.kt
├── main
├── AndroidManifest.xml
├── ic_launcher-playstore.png
├── java
│ └── com
│ │ └── frank
│ │ └── glyphify
│ │ ├── AppUpdater.kt
│ │ ├── BootReceiver.kt
│ │ ├── Constants.kt
│ │ ├── MainActivity.kt
│ │ ├── PermissionHandling.kt
│ │ ├── Util.kt
│ │ ├── glyph
│ │ ├── batteryindicator
│ │ │ ├── BatteryIndicatorService.kt
│ │ │ └── PowerConnectionReceiver.kt
│ │ ├── composer
│ │ │ ├── BeatDetector.kt
│ │ │ ├── FileHandling.kt
│ │ │ ├── Glyphifier.kt
│ │ │ ├── LightEffects.kt
│ │ │ └── PatternFinder.kt
│ │ ├── extendedessential
│ │ │ ├── Animations.kt
│ │ │ ├── ExtendedEssentialReceiver.kt
│ │ │ └── ExtendedEssentialService.kt
│ │ └── visualizer
│ │ │ └── GlyphVisualizer.kt
│ │ └── ui
│ │ ├── dialogs
│ │ └── Dialog.kt
│ │ ├── home
│ │ └── HomeFragment.kt
│ │ ├── notifications
│ │ ├── AppsChoiceFragment.kt
│ │ ├── ContactsChoiceFragment.kt
│ │ ├── NotificationsFragment.kt
│ │ └── TimePickerFragment.kt
│ │ └── ringtones
│ │ └── RingtonesFragment.kt
├── python
│ └── beatDetectorFun.py
└── res
│ ├── anim
│ ├── fade_in.xml
│ └── fade_out.xml
│ ├── color
│ └── bottom_nav_item_color.xml
│ ├── drawable
│ ├── circle.xml
│ ├── ic_add.xml
│ ├── ic_add_person.xml
│ ├── ic_apply.xml
│ ├── ic_apps.xml
│ ├── ic_back.xml
│ ├── ic_bin.xml
│ ├── ic_glyphifier.xml
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_notifications.xml
│ ├── ic_pause.xml
│ ├── ic_play.xml
│ ├── ic_ringtones_lib.xml
│ ├── ic_share.xml
│ ├── ic_single.xml
│ ├── ic_sleep.xml
│ ├── ic_sunlight.xml
│ ├── p1_glyph.png
│ ├── p2_glyph.png
│ ├── p2a_glyph.png
│ ├── rounded_button_gray.xml
│ ├── rounded_button_red.xml
│ ├── rounded_dialog.xml
│ └── style_expanded_toggle.xml
│ ├── font
│ └── dot_matri.TTF
│ ├── layout
│ ├── activity_main.xml
│ ├── dialog_error.xml
│ ├── dialog_exact_alarm.xml
│ ├── dialog_notification_listener_disclaimer.xml
│ ├── dialog_output_name.xml
│ ├── dialog_perm_contacts.xml
│ ├── dialog_perm_notifications.xml
│ ├── dialog_perm_settings.xml
│ ├── dialog_sleep_mode.xml
│ ├── first_boot.xml
│ ├── fragment_apps_choice.xml
│ ├── fragment_contacts_choice.xml
│ ├── fragment_home.xml
│ ├── fragment_notifications_1.xml
│ ├── fragment_notifications_2.xml
│ ├── fragment_notifications_2a.xml
│ ├── fragment_ringtones.xml
│ ├── item_ringtone.xml
│ └── item_rr_centered_btn.xml
│ ├── menu
│ └── bottom_nav_menu.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── navigation
│ └── mobile_navigation.xml
│ ├── values-de
│ └── strings.xml
│ ├── values-it
│ └── strings.xml
│ ├── values-ja
│ └── strings.xml
│ ├── values-night
│ └── themes.xml
│ ├── values-tr
│ └── strings.xml
│ ├── values-zh-rCN
│ └── strings.xml
│ ├── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ ├── data_extraction_rules.xml
│ └── file_paths.xml
└── test
└── java
└── com
└── frank
└── glyphify
└── ExampleUnitTest.kt
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Glyphify v2: available now on Play Store
3 |
4 |
5 | [Deutsch](./README_DE.md) | [日本語](./README_JA.md)
6 | ## Introduction
7 | **Glyphify** is an **Android app made for Nothing Phones**, it **packs multiple features to make the Glyph interface more useful**:
8 |
9 | ### Glyphifier
10 | It's an automatic composer, choose any song (.mp3, wav etc) and it will be converted into a Glyph synched ringtone with a better accuracy compared to the built-in Nothing algorithm. Phone(2) users can also enjoy all 33 zones addressing for their ringtones. Songs are also randomic, meaning that by using the same source file you will get a different ringtone each time, choose which one you like the most!
11 |
12 |
13 |
14 |
15 |
16 | https://github.com/user-attachments/assets/e40d1011-4951-478c-a690-fe725a1b3276
17 |
18 | ### Phone(2a)'s battery indicator
19 | A missing feature present on other Nothing phones it's back thanks to Glyphify
20 |
21 | https://github.com/user-attachments/assets/b8874ed1-3cba-4fbc-b834-ed0731632519
22 |
23 | ### Extended Nothing Essential notifications
24 | Why use only one Glyph as an Essential notification LED? With Glyphify you can **map any Glyph zone to multiple contacts and apps** and also chose between two different light effect on a per zone basis: static and pulsing glyph
25 |
26 |
27 |
28 | https://github.com/user-attachments/assets/2356ccf4-8219-457c-824b-2ed0e7514db2
29 |
30 | ## Compatibility
31 | The app has been tested on Nothing Phone(1), Phone(2) and Phone(2a).
32 |
33 | ## Donors
34 |
35 | A huge thanks to everyone which believes in this project and shows it by donating:
36 | - **Mirko D. - 98.76€** (**check his _Nothing Community app_ on the play store** [here](https://play.google.com/store/apps/details?id=com.nothing.news))
37 | - **James Z. - 10.71€**
38 | - **Gregor F. - 10.71€**
39 | - **Vadim D. - 7.5€**
40 | - **Landon C. - 6.57€**
41 | - **Robert F. - 5.54€**
42 | - **Andrew A. - 5.54€**
43 | - **Gregor B. - 5.54€**
44 | - **Kilian B. - 5.54€**
45 | - **René H. - 5.54€**
46 | - **Daniel L.A. - 5.54€**
47 | - **Simona D.C. - 2.95€**
48 | - **Boris B. - 2.95€**
49 | - **Nathan B. - 2.95€**
50 | - **Marcin K. - 2.5€**
51 | - **Christian G. - 2.5€**
52 | - **Bridget W. - 1.4€**
53 |
54 |
55 | **This app is free and ad-free**.\
56 | **If you value my work** and want to support me **consider starring this repo or donating (any amount), it would help me a lot and keep me adding new features**!
57 |
58 | [](https://www.paypal.com/donate/?hosted_button_id=HJU8Y7F34Z6TL)
59 |
60 | ## Allow Restricted Settings
61 | Without "Allow Restricted Settings", notifications cannot be allowed.
62 |
63 | `Settings` -> `Apps` -> `Recently opend apps` -> `Glyphify` -> `Tap to 3Dot Menu` -> `Allow Restricted Settings`
64 |
65 | ## Download
66 |
67 | You can **download** the app from the [release](https://github.com/Fr4nKB/Glyphify/releases/latest) panel
68 |
--------------------------------------------------------------------------------
/README_DE.md:
--------------------------------------------------------------------------------
1 | # Glyphify: multifunktionale Glyph app
2 | [English](./README.md) | [日本語](./README_JA.md)
3 | ## Einführung
4 | **Glyphify** ist eine **Android App für Nothing Phones**, sie **beinhaltet einige Funktionen, um das Glyph Interface nützlicher zu machen**:
5 |
6 | ### Glyphifier
7 | Dies ist ein automatischer Composer, wähle einen beliebigen Musiktitel (.mp3, wav etc), der dann in einen mit den Glyphs synchronisierten Klingelton umgewandelt wird. Das Ergebnis wird jedoch genauer sein als der Algorithmus von Nothing. Phone(2) Nutzer können dabei sogar alle 33 Zonen für ihre Klingeltöne nutzen. Die Musiktitel werden außerdem zufällig umgewandelt, das heißt für die selbe Datei wird die App jedes Mal einen unterschiedlichen Glyph Klingelton erstellen. Such' dir den aus, der dir am besten gefällt!
8 |
9 |
10 |
11 |
12 |
13 | https://github.com/user-attachments/assets/e40d1011-4951-478c-a690-fe725a1b3276
14 |
15 | ### Phone(2a)'s Akkuladeanzeige
16 | Eine oft vermisste Funktion, die es bei anderen Nothing Smartphones gibt ist dank Glyphify zurück
17 |
18 | https://github.com/user-attachments/assets/b8874ed1-3cba-4fbc-b834-ed0731632519
19 |
20 | ### Erweiterte Nothing Essential Benachrichtigungen
21 | Warum nur ein Glyph als Essential Benachrichtigungs-LED nutzen? mit Glyphify kannst du hy use only one Glyph as an Essential notification LED? With Glyphify you can **jede Glyph Zone mehrfachen Kontakten und Apps zuordnen** und sogar zwischen zwei unterschiedlichen Lichteffekten je Zone wählen: statisches und pulsierendes Glyph
22 |
23 |
24 |
25 | https://github.com/user-attachments/assets/2356ccf4-8219-457c-824b-2ed0e7514db2
26 |
27 | ## Kompatibilität
28 | Die App wurde auf Nothing Phone(1), Phone(2) and Phone(2a) getestet.
29 |
30 | ## Unterstützer
31 |
32 | Ein riesiges Dankeschön an jeden, der an dieses Projekt glaubt und dies durch eine Spende zeigt:
33 | - **Mirko D. - 98.76€** (**check his _Nothing Community app_ on the play store** [here](https://play.google.com/store/apps/details?id=com.nothing.news))
34 | - **Landon C. - 6.57€**
35 | - **Robert F. - 5.54€**
36 | - **Andrew A. - 5.54€**
37 | - **Gregor B. - 5.54€**
38 | - **Kilian B. - 5.54€**
39 | - **René H. - 5.54€**
40 | - **Vadim D. - 5€**
41 | - **Simona D.C. - 2.95€**
42 | - **Boris B. - 2.95€**
43 | - **Bridget W. - 1.4€**
44 |
45 |
46 | **Diese App ist kostenlos und werbefrei**.\
47 | **Wenn dir meine Arbeit etwas wert ist** und du mich unterstützen möchtest **denke über einen Stern für dieses Repo nach oder eine Spende (jede Summe), dies hilft mir sehr und lässt mich neue Funktionen hinzufügen**!
48 |
49 | [](https://www.paypal.com/donate/?hosted_button_id=HJU8Y7F34Z6TL)
50 |
51 | ## Eingeschränkte Einstellungen zulassen
52 | ohne "Eingeschränkte Einstellungen zulassen", können die Benachrichtigungen nicht angezeigt werden.
53 |
54 | `Einstellungen` -> `Apps` -> `Kürzlich geöffnete Apps` -> `Glyphify` -> `Tippe auf das 3-Punkte Menü` -> `Eingeschränkte Einstellungen zulassen`
55 |
56 | ## Download
57 |
58 | Du kannst die App im [release](https://github.com/Fr4nKB/Glyphify/releases/latest) Bereich **herunterladen**
59 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 | # Glyphify: 多目的な Glyph アプリ
2 | [English](./README.md) | [Deutsch](./README_DE.md)
3 | ## このアプリについて
4 | **Glyphify** は **Nothing Phone 用に作られた Android アプリ** で **Glyph インターフェースをより便利にする複数の機能を備えています**:
5 |
6 | ### Glyphifier
7 | 自動作曲機能で、任意の曲 (.mp3 や .wav など) を選択すると内蔵の Nothing アルゴリズムよりも高精度な Glyph 同期着信音に変換されます。Phone(2) のユーザーは、着信音として 33 個のゾーンすべてに対応させることも可能です。ランダムで生成されるので同じソースとなる曲を使用しても、毎回異なる着信音が生成されます。最も気に入った物を選んでください。
8 |
9 |
10 |
11 |
12 |
13 | https://github.com/user-attachments/assets/e40d1011-4951-478c-a690-fe725a1b3276
14 |
15 | ### Phone(2a) バッテリーインジケーター
16 | 他の Nothing Phone で欠けていた機能が Glyphify で蘇ります
17 |
18 | https://github.com/user-attachments/assets/b8874ed1-3cba-4fbc-b834-ed0731632519
19 |
20 | ### 拡張された Nothing Essential 通知
21 | Essential 通知の LED として Glyph を 1 つだけ使用するのは疑問に思いませんか?
22 | Glyphify を使用することで**任意の Glyph ゾーンを連絡先やアプリにマッピング**、2 種類の異なるライトの効果をゾーンに割り当てられます。
23 |
24 | 選択可能なライトの効果: 静的な Glyph とパルス効果の Glyph
25 |
26 |
27 |
28 | https://github.com/user-attachments/assets/2356ccf4-8219-457c-824b-2ed0e7514db2
29 |
30 | ## 互換性
31 | このアプリは、Nothing Phone (1)、Phone(2) と Phone(2a) でテストされています。
32 |
33 | ## 寄付者
34 |
35 | このプロジェクトに寄付で貢献してくださった皆様に心から感謝いたします:
36 | - **Mirko D. - 98.76€** (**check his _Nothing Community app_ on the play store** [here](https://play.google.com/store/apps/details?id=com.nothing.news))
37 | - **Landon C. - 6.57€**
38 | - **Robert F. - 5.54€**
39 | - **Andrew A. - 5.54€**
40 | - **Gregor B. - 5.54€**
41 | - **Kilian B. - 5.54€**
42 | - **René H. - 5.54€**
43 | - **Vadim D. - 5€**
44 | - **Simona D.C. - 2.95€**
45 | - **Boris B. - 2.95€**
46 | - **Bridget W. - 1.4€**
47 |
48 |
49 | **このアプリは無料であり広告はありません**。\
50 | もしも **私が開発した物が評価できると思えたり、サポートをしたいのであれば** このリポジトリに Star を付けるか、寄付 (金額は問いません) することをご検討ください!
51 |
52 | [](https://www.paypal.com/donate/?hosted_button_id=HJU8Y7F34Z6TL)
53 |
54 | ## 制限付き設定の許可
55 | 「制限付き設定の許可」を行なわないと通知のアクセス許可を操作できません。
56 |
57 | `設定` -> `アプリ` -> `最近開いたアプリ` -> `Glyphify` -> `3 つのドットメニューボタンをタップ` -> `制限付き設定を許可`
58 |
59 | ## ダウンロード
60 |
61 | [release](https://github.com/Fr4nKB/Glyphify/releases/latest) のパネルからアプリを **ダウンロード** できます。
62 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.jetbrains.kotlin.android)
4 | id("com.chaquo.python")
5 | }
6 |
7 | android {
8 | namespace = "com.frank.glyphify"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | applicationId = "com.frank.glyphify"
13 | minSdk = 34
14 | targetSdk = 34
15 | versionCode = 1
16 | versionName = "1.4.2"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | ndk {
20 | abiFilters += listOf("arm64-v8a")
21 | }
22 |
23 | resValue("string", "NOTHING_API_KEY", "\"${System.getenv("NOTHING_API_KEY")}\"")
24 | }
25 |
26 | buildTypes {
27 | release {
28 | isMinifyEnabled = true
29 | isShrinkResources = true
30 | proguardFiles(
31 | getDefaultProguardFile("proguard-android-optimize.txt"),
32 | "proguard-rules.pro"
33 | )
34 | }
35 | }
36 |
37 | compileOptions {
38 | sourceCompatibility = JavaVersion.VERSION_1_8
39 | targetCompatibility = JavaVersion.VERSION_1_8
40 | }
41 |
42 | kotlinOptions {
43 | jvmTarget = "1.8"
44 | }
45 |
46 | buildFeatures {
47 | viewBinding = true
48 | }
49 |
50 | }
51 |
52 | chaquopy {
53 | defaultConfig {
54 | buildPython(System.getenv("PYTHON38_PATH"))
55 | version = "3.8"
56 | pip {
57 | install("numpy==1.19.5")
58 | install("numba==0.48.0")
59 | install("joblib==1.0.0")
60 | install("resampy==0.2.2")
61 | install("librosa==0.7.2")
62 | }
63 | }
64 | productFlavors { }
65 | sourceSets { }
66 | }
67 |
68 | dependencies {
69 | implementation(libs.androidx.core.ktx)
70 | implementation(libs.androidx.appcompat)
71 | implementation(libs.material)
72 | implementation(libs.androidx.constraintlayout)
73 | implementation(libs.androidx.lifecycle.livedata.ktx)
74 | implementation(libs.androidx.lifecycle.viewmodel.ktx)
75 | implementation(libs.androidx.navigation.fragment.ktx)
76 | implementation(libs.androidx.navigation.ui.ktx)
77 | implementation(libs.androidx.work.runtime.ktx)
78 | testImplementation(libs.junit)
79 | androidTestImplementation(libs.androidx.junit)
80 | androidTestImplementation(libs.androidx.espresso.core)
81 |
82 | implementation(libs.ffmpeg.kit.audio)
83 | implementation(libs.okhttp)
84 | implementation(libs.smile.core)
85 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
86 | }
87 |
--------------------------------------------------------------------------------
/app/libs/KetchumSDK_Community_20240307.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/libs/KetchumSDK_Community_20240307.jar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # Keep classes for okhttp3
24 | -keep class org.bouncycastle.jsse.** { *; }
25 | -keep class org.conscrypt.** { *; }
26 | -keep class org.openjsse.** { *; }
27 |
28 | # DontWarn rules for okhttp3
29 | -dontwarn org.bouncycastle.jsse.**
30 | -dontwarn org.conscrypt.**
31 | -dontwarn org.openjsse.**
32 | -dontwarn org.slf4j.impl.StaticLoggerBinder
33 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/frank/glyphify/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.frank.glyphify", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
78 |
79 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/AppUpdater.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import android.app.PendingIntent
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.util.Log
8 | import androidx.core.app.NotificationCompat
9 | import androidx.core.app.NotificationManagerCompat
10 | import androidx.work.Worker
11 | import androidx.work.WorkerParameters
12 | import okhttp3.OkHttpClient
13 | import okhttp3.Request
14 | import org.json.JSONObject
15 |
16 | class AppUpdater(private val context: Context, workerParams: WorkerParameters):
17 | Worker(context, workerParams) {
18 |
19 | companion object {
20 | val versionUrl = "https://api.github.com/repos/Fr4nKB/Glyphify/releases/latest"
21 | val downloadUrl = "https://github.com/Fr4nKB/Glyphify/releases/latest"
22 | }
23 |
24 | override fun doWork(): Result {
25 | val client = OkHttpClient()
26 | val request = Request.Builder().url(versionUrl).build()
27 |
28 | client.newCall(request).execute().use { response ->
29 | if (!response.isSuccessful) return Result.failure()
30 |
31 | val jsonObject = JSONObject(response.body!!.string())
32 | val latestVersion = jsonObject.getString("tag_name").substring(1)
33 |
34 | // get the current version of the app
35 | val currentVersion = context.packageManager.getPackageInfo(context.packageName, 0)
36 | .versionName
37 |
38 | val current = currentVersion.split(".").map { it.toInt() }
39 | val latest = latestVersion.split(".").map { it.toInt() }
40 |
41 | val minLength = minOf(current.size, latest.size)
42 | var update = latest.size > current.size
43 |
44 | // compare the two versions
45 | for (i in 0 until minLength) {
46 | val currentPart = current.getOrElse(i) { 0 }
47 | val latestPart = latest.getOrElse(i) { 0 }
48 |
49 | if (currentPart < latestPart) {
50 | update = true
51 | break
52 | }
53 | else if(currentPart > latestPart) return Result.success()
54 | }
55 |
56 | if(update) {
57 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl))
58 | val pendingIntent = PendingIntent.getActivity(context, 0, browserIntent,
59 | PendingIntent.FLAG_IMMUTABLE)
60 |
61 | val builder = NotificationCompat.Builder(context, context.getString(R.string.app_name))
62 | .setSmallIcon(R.drawable.ic_launcher_foreground)
63 | .setContentTitle(context.getString(R.string.title_new_version))
64 | .setContentText(context.getString(R.string.msg_new_version))
65 | .setPriority(NotificationCompat.PRIORITY_DEFAULT)
66 | .setContentIntent(pendingIntent)
67 | .setAutoCancel(true)
68 |
69 | with(NotificationManagerCompat.from(context)) {
70 | notify(1, builder.build())
71 | }
72 | }
73 | }
74 |
75 |
76 | return Result.success()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/BootReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.SharedPreferences
7 | import android.os.Build
8 | import androidx.work.OneTimeWorkRequestBuilder
9 | import androidx.work.PeriodicWorkRequest
10 | import androidx.work.WorkManager
11 | import com.frank.glyphify.Constants.PHONE2A_MODEL_ID
12 | import com.frank.glyphify.Util.exactAlarm
13 | import com.frank.glyphify.glyph.batteryindicator.BatteryIndicatorService
14 | import java.util.concurrent.TimeUnit
15 |
16 | class BootReceiver: BroadcastReceiver() {
17 | override fun onReceive(context: Context, intent: Intent) {
18 | val action = intent.action
19 |
20 | if(action == Intent.ACTION_BOOT_COMPLETED) {
21 |
22 | if(Build.MODEL == PHONE2A_MODEL_ID) {
23 | val serviceIntent = Intent(context, BatteryIndicatorService::class.java)
24 | context.startService(serviceIntent)
25 | }
26 |
27 | val singleWorkReq = OneTimeWorkRequestBuilder()
28 | .build()
29 | WorkManager.getInstance(context).enqueue(singleWorkReq)
30 |
31 | val periodicWorkRequest = PeriodicWorkRequest.Builder(AppUpdater::class.java, 4, TimeUnit.HOURS)
32 | .build()
33 | WorkManager.getInstance(context).enqueue(periodicWorkRequest)
34 |
35 | val sharedPref: SharedPreferences =
36 | context.getSharedPreferences("settings", Context.MODE_PRIVATE)
37 |
38 | val isSleepModeActive = sharedPref.getBoolean("isSleepModeActive", false)
39 | if(isSleepModeActive) {
40 | exactAlarm(context, "SLEEP_ON", 1)
41 | exactAlarm(context, "SLEEP_OFF", 1)
42 | }
43 | }
44 |
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | object Constants {
4 | const val GLYPH_DEFAULT_INTENSITY = 4000
5 | const val GLYPH_MID_INTENSITY = 2500
6 | const val GLYPH_MAX_INTENSITY = 4095
7 | const val LIGHT_DURATION_US = 16666
8 | const val GLYPHIFY_COMPOSER_PATTERN = "-0,-1,-2,-3,-4,c-0,-4,c-0,-3,-4,s-0,-1,-2,-3,-4,c-4,c-4,s-0," +
9 | "-1,-2,-4,c-2,-4,c-0,-1,-2,-3,-4,s-0,-1,-2,-3,-4,c-0,-2,c-0,-1,-2,s-0,-1,-2,-3," +
10 | "-4,c-2,c-0,-1,-2,-3,-4,s-0,-1,-2,-3,-4,s-0,-1,-2,-3,-4,c-0,-2,c-0,s-0,-1,-2,-4," +
11 | "c-2,-4,c-0,-1,-2,-3,-4"
12 | const val ALBUM_NAME = "v1-Glyphify"
13 |
14 | const val CHANNEL_ID = "Glyphify"
15 |
16 | const val PHONE1_MODEL_ID = "A063"
17 | val PHONE2_MODEL_ID = listOf("A065", "AIN065")
18 | const val PHONE2A_MODEL_ID = "A142"
19 |
20 | const val EE_ANIMATIONS_NUM = 4
21 | val DIMMING_VALUES = listOf(1000, 2500, 4000, -1)
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import android.Manifest
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.content.SharedPreferences
9 | import android.graphics.Color
10 | import android.graphics.drawable.ColorDrawable
11 | import android.os.Build
12 | import android.os.Bundle
13 | import android.os.Environment
14 | import androidx.appcompat.app.AppCompatActivity
15 | import androidx.core.content.ContextCompat
16 | import androidx.navigation.findNavController
17 | import androidx.navigation.ui.setupWithNavController
18 | import com.frank.glyphify.Constants.CHANNEL_ID
19 | import com.frank.glyphify.Constants.PHONE1_MODEL_ID
20 | import com.frank.glyphify.Constants.PHONE2A_MODEL_ID
21 | import com.frank.glyphify.Constants.PHONE2_MODEL_ID
22 | import com.frank.glyphify.databinding.ActivityMainBinding
23 | import com.frank.glyphify.glyph.batteryindicator.BatteryIndicatorService
24 | import com.frank.glyphify.glyph.visualizer.GlyphVisualizer
25 | import com.frank.glyphify.ui.dialogs.Dialog.supportMe
26 | import com.google.android.material.bottomnavigation.BottomNavigationView
27 | import java.io.File
28 | import kotlin.random.Random
29 |
30 |
31 | class MainActivity : AppCompatActivity() {
32 |
33 | private lateinit var binding: ActivityMainBinding
34 |
35 | private fun setComposerAppVersion(): String {
36 | val manufacturer = Build.MANUFACTURER
37 | val model = Build.MODEL
38 |
39 | val sharedPref: SharedPreferences =
40 | this.getSharedPreferences("settings", Context.MODE_PRIVATE)
41 | val editor: SharedPreferences.Editor = sharedPref.edit()
42 |
43 | if(manufacturer.equals("Nothing", ignoreCase = true)) {
44 | if(model.equals(PHONE1_MODEL_ID)) {
45 | editor.putString("appVersion", "v1-Spacewar Glyph Composer")
46 | editor.apply()
47 | }
48 | else if(PHONE2_MODEL_ID.contains(model)) {
49 | editor.putString("appVersion", "v1-Pong Glyph Composer")
50 | editor.apply()
51 | }
52 | else if(model.equals(PHONE2A_MODEL_ID)) {
53 | editor.putString("appVersion", "v1-Pacman Glyph Composer")
54 | editor.apply()
55 | }
56 | }
57 |
58 | return "0"
59 | }
60 |
61 | private fun createNotificationChannel() {
62 | val importance = NotificationManager.IMPORTANCE_DEFAULT
63 | val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance)
64 |
65 | val notificationManager: NotificationManager =
66 | getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
67 | notificationManager.createNotificationChannel(channel)
68 | }
69 |
70 | private fun welcomeMsg() {
71 | val sharedPref: SharedPreferences =
72 | this.getSharedPreferences("settings", Context.MODE_PRIVATE)
73 |
74 | val isFirstBoot = sharedPref.getBoolean("firstboot", true)
75 | val randomNumber = Random.nextInt(1, 11)
76 |
77 | val permHandler = PermissionHandling(this)
78 |
79 | if(isFirstBoot || randomNumber == 1) {
80 | supportMe(this, permHandler)
81 | if (isFirstBoot) {
82 | val editor: SharedPreferences.Editor = sharedPref.edit()
83 | editor.putBoolean("firstboot", false)
84 | editor.apply()
85 | }
86 | }
87 | else {
88 | val permissions = mutableListOf(
89 | Manifest.permission.POST_NOTIFICATIONS
90 | )
91 | permHandler.askRequiredPermissions(permissions, R.layout.dialog_perm_notifications)
92 | }
93 | }
94 |
95 | override fun onCreate(savedInstanceState: Bundle?) {
96 | super.onCreate(savedInstanceState)
97 |
98 | binding = ActivityMainBinding.inflate(layoutInflater)
99 | setContentView(binding.root)
100 |
101 | val navView: BottomNavigationView = binding.navView
102 | val bottomNavigationView = findViewById(R.id.nav_view)
103 | bottomNavigationView.setBackgroundColor(ContextCompat.getColor(this, R.color.black))
104 | bottomNavigationView.itemBackground = ColorDrawable(Color.TRANSPARENT)
105 |
106 | val navController = findNavController(R.id.nav_host_fragment_activity_main)
107 | navView.setupWithNavController(navController)
108 |
109 | setComposerAppVersion()
110 | createNotificationChannel()
111 |
112 | welcomeMsg()
113 |
114 | if(Build.MODEL == PHONE2A_MODEL_ID) {
115 | val intent = Intent(this, BatteryIndicatorService::class.java)
116 | startService(intent)
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/PermissionHandling.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.pm.PackageManager
6 | import androidx.core.app.ActivityCompat
7 | import androidx.core.content.ContextCompat
8 | import com.frank.glyphify.ui.dialogs.Dialog
9 |
10 | class PermissionHandling(private val activity: Activity) {
11 |
12 | private fun askPermissions(permissions: Array, requestCode: Int) {
13 | ActivityCompat.requestPermissions(activity, permissions, requestCode)
14 | }
15 |
16 | /**shows alert dialog, if ok is pressed a set of permissions are requested otherwise the app is closed*/
17 | private fun popUp(permissions: Array, layout: Int, requestCode: Int) {
18 | Dialog.showDialog(
19 | activity,
20 | layout,
21 | mapOf(
22 | R.id.positiveButton to {
23 | askPermissions(permissions, requestCode)
24 | },
25 | R.id.negativeButton to { }
26 | )
27 | )
28 | }
29 |
30 | fun checkRequiredPermission(permissions: List): Boolean {
31 | for (permission in permissions) {
32 | if (ContextCompat.checkSelfPermission(activity, permission)
33 | != PackageManager.PERMISSION_GRANTED) {
34 | return false
35 | }
36 | }
37 | return true
38 | }
39 |
40 | fun askRequiredPermissions(permissions: List, layout: Int) {
41 | if (!checkRequiredPermission(permissions)) {
42 | popUp(permissions.toTypedArray(), layout, 1)
43 | }
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/Util.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import android.app.AlarmManager
4 | import android.app.PendingIntent
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.SharedPreferences
8 | import android.content.res.Resources
9 | import android.graphics.Bitmap
10 | import android.graphics.Canvas
11 | import android.graphics.drawable.BitmapDrawable
12 | import android.graphics.drawable.Drawable
13 | import com.frank.glyphify.glyph.extendedessential.ExtendedEssentialService
14 | import org.json.JSONArray
15 | import org.json.JSONObject
16 | import java.math.BigInteger
17 | import java.util.Calendar
18 |
19 | object Util {
20 | fun loadPreferences(context: Context, numZones: Int): MutableList, Int>> {
21 | val sharedPref: SharedPreferences =
22 | context.getSharedPreferences("settings", Context.MODE_PRIVATE)
23 | val jsonString = sharedPref.getString("contactsMapping", null)
24 | val glyphsMapping = MutableList(numZones) { Triple(-1, emptyList(), 0) }
25 |
26 | if (jsonString != null) {
27 | val mapping = JSONObject(jsonString)
28 | for (i in 0 until numZones) {
29 | if (mapping.has(i.toString())) {
30 | val jsonArray = mapping.getJSONArray(i.toString())
31 | val glyphId = jsonArray.getInt(0)
32 |
33 | var pulse: Int
34 | try {
35 | pulse = jsonArray.getInt(2)
36 | }
37 | catch (e: Exception) {
38 | pulse = if (jsonArray.getBoolean(2)) 1 else 0
39 | }
40 |
41 | var mappingIdsJsonArray = JSONArray()
42 | try {
43 | mappingIdsJsonArray = jsonArray.getJSONArray(1)
44 | } catch (_: Exception) {}
45 |
46 | val mappingIds = mutableListOf()
47 | for (j in 0 until mappingIdsJsonArray.length()) {
48 | mappingIds.add(BigInteger(mappingIdsJsonArray.getString(j)))
49 | }
50 |
51 | glyphsMapping[i] = Triple(glyphId, mappingIds.toList(), pulse)
52 | }
53 | }
54 | }
55 |
56 | return glyphsMapping
57 | }
58 |
59 | fun resizeDrawable(resources: Resources, drawable: Drawable, width: Int, height: Int): Drawable {
60 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
61 | val canvas = Canvas(bitmap)
62 | drawable.setBounds(0, 0, canvas.width, canvas.height)
63 | drawable.draw(canvas)
64 | return BitmapDrawable(resources, bitmap)
65 | }
66 |
67 | fun fromStringToNum(packageName: String): BigInteger {
68 | return -BigInteger(packageName.toByteArray())
69 | }
70 |
71 | fun fromNumToString(appId: BigInteger): String {
72 | return String((-appId).toByteArray())
73 | }
74 |
75 | fun exactAlarm(context: Context, intentAction: String, setAction: Int) {
76 | val sharedPref = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
77 |
78 | var time = ""
79 | if(intentAction == "SLEEP_ON") time = sharedPref.getString("sleepStart", "") ?: ""
80 | else if(intentAction == "SLEEP_OFF") time = sharedPref.getString("sleepEnd", "") ?: ""
81 |
82 | if (time.isNotEmpty()) {
83 | val timeParts = time.split(":")
84 | val hour = timeParts[0].toInt()
85 | val minute = timeParts[1].toInt()
86 |
87 | val calendar = Calendar.getInstance().apply {
88 | set(Calendar.HOUR_OF_DAY, hour)
89 | set(Calendar.MINUTE, minute)
90 | set(Calendar.SECOND, 0)
91 | set(Calendar.MILLISECOND, 0)
92 | }
93 |
94 | val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
95 | val intent = Intent(context, ExtendedEssentialService::class.java).apply {
96 | action = intentAction
97 | }
98 | val pendingIntent = PendingIntent.getService(context, 0, intent,
99 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
100 |
101 | alarmManager.cancel(pendingIntent)
102 | if(setAction in 1..2) {
103 | val alarmTime: Long
104 | if(setAction == 1) alarmTime = calendar.timeInMillis
105 | else alarmTime = calendar.timeInMillis + AlarmManager.INTERVAL_DAY
106 |
107 | alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTime, pendingIntent)
108 | }
109 | }
110 | }
111 |
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/batteryindicator/BatteryIndicatorService.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.batteryindicator
2 |
3 | import android.app.Notification
4 | import android.app.Service
5 | import android.content.ComponentName
6 | import android.content.ContentValues.TAG
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.IntentFilter
10 | import android.hardware.Sensor
11 | import android.hardware.SensorEvent
12 | import android.hardware.SensorEventListener
13 | import android.hardware.SensorManager
14 | import android.os.BatteryManager
15 | import android.os.Handler
16 | import android.os.IBinder
17 | import android.os.Looper
18 | import android.os.PowerManager
19 | import android.util.Log
20 | import com.frank.glyphify.Constants.CHANNEL_ID
21 | import com.frank.glyphify.R
22 | import com.frank.glyphify.glyph.extendedessential.ExtendedEssentialService
23 | import com.nothing.ketchum.Common
24 | import com.nothing.ketchum.Glyph
25 | import com.nothing.ketchum.GlyphException
26 | import com.nothing.ketchum.GlyphManager
27 | import kotlinx.coroutines.CoroutineScope
28 | import kotlinx.coroutines.Dispatchers
29 | import kotlinx.coroutines.delay
30 | import kotlinx.coroutines.launch
31 |
32 |
33 | class BatteryIndicatorService : Service() {
34 | private var mGM: GlyphManager? = null
35 | private var mCallback: GlyphManager.Callback? = null
36 | private lateinit var sensorEventListener: SensorEventListener
37 | private var lastShakeTime: Long = 0
38 | private val serviceScope = CoroutineScope(Dispatchers.Default)
39 | private lateinit var wakeLock: PowerManager.WakeLock
40 | private lateinit var powerConnectionReceiver: PowerConnectionReceiver
41 |
42 | // hold sensor values of the previous event
43 | var lastX = 0f
44 | var lastY = 0f
45 | var lastZ = 0f
46 |
47 | private fun getBatteryPercentage(context: Context): Int {
48 | val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
49 | context.registerReceiver(null, ifilter)
50 | }
51 | val level: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
52 | val scale: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
53 |
54 | return (level / scale.toFloat() * 100).toInt()
55 | }
56 |
57 | private fun signalEE() {
58 | val serviceIntent = Intent(this, ExtendedEssentialService::class.java)
59 | serviceIntent.action = "SHOW_GLYPHS"
60 | startService(serviceIntent)
61 | }
62 |
63 | private fun registerSensorListener() {
64 | val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
65 |
66 | sensorEventListener = object : SensorEventListener {
67 | val shakeThreshold = 0.25
68 |
69 | override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
70 | }
71 |
72 | override fun onSensorChanged(event: SensorEvent) {
73 | if(event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
74 | val x = event.values[0]
75 | val y = event.values[1]
76 | val z = event.values[2]
77 |
78 | // calculate the force of shake
79 | val deltaX = x - lastX
80 | val deltaY = y - lastY
81 | val deltaZ = z - lastZ
82 | val shakeForce = Math.abs(deltaX + deltaY + deltaZ)
83 |
84 | lastX = x
85 | lastY = y
86 | lastZ = z
87 |
88 | // check if the device is approximately horizontal and facing down
89 | if(z < -9.8 && (Math.abs(z) - 9.8) < 1 && Math.abs(x) < 2 && Math.abs(y) < 2) {
90 |
91 | val currentTime = System.currentTimeMillis()
92 |
93 | if(shakeForce > shakeThreshold && currentTime - lastShakeTime > 3500) {
94 | lastShakeTime = currentTime
95 |
96 | val builder = mGM!!.glyphFrameBuilder
97 | val batteryLevel = getBatteryPercentage(applicationContext)
98 | val batteryIndicatorFrame = builder.buildChannel(Glyph.Code_23111.C_1).build()
99 |
100 | serviceScope.launch {
101 | try {
102 | val wakeLockTime = 10 * 1000
103 |
104 | if(!wakeLock.isHeld) wakeLock.acquire(wakeLockTime.toLong())
105 |
106 | mGM?.openSession()
107 | for(i in 0..batteryLevel step 10) {
108 | mGM?.displayProgressAndToggle(
109 | batteryIndicatorFrame,
110 | i,
111 | false)
112 |
113 | delay(30)
114 | }
115 |
116 | delay(3000)
117 | mGM?.turnOff()
118 | signalEE()
119 | }
120 | finally {
121 | if(wakeLock.isHeld) wakeLock.release()
122 | mGM?.closeSession()
123 | }
124 | }
125 |
126 | }
127 |
128 | }
129 |
130 | }
131 | }
132 | }
133 |
134 | sensorManager.registerListener(
135 | sensorEventListener,
136 | sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
137 | SensorManager.SENSOR_DELAY_NORMAL
138 | )
139 | }
140 |
141 | private fun unregisterSensorListener() {
142 | try {
143 | val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
144 | sensorManager.unregisterListener(sensorEventListener)
145 | }
146 | catch (_: Exception) {}
147 | }
148 |
149 | override fun onBind(intent: Intent): IBinder? {
150 | return null
151 | }
152 |
153 | override fun onCreate() {
154 | super.onCreate()
155 |
156 | Handler(Looper.getMainLooper()).postDelayed({
157 | init()
158 | val localGM = GlyphManager.getInstance(applicationContext)
159 | localGM?.init(mCallback)
160 | mGM = localGM
161 |
162 | // turn off glyphs in case some of them were stuck
163 | mGM?.openSession()
164 | mGM?.turnOff()
165 | mGM?.closeSession()
166 | }, 3 * 1000)
167 |
168 | val powerManager = getSystemService(POWER_SERVICE) as PowerManager
169 | wakeLock = powerManager.newWakeLock(
170 | PowerManager.PARTIAL_WAKE_LOCK,
171 | "Glyhpify::BatteryIndicator"
172 | )
173 |
174 | // use this service to register Phone(2a)'s battery indicator
175 | powerConnectionReceiver = PowerConnectionReceiver()
176 | val powerFilter = IntentFilter().apply {
177 | addAction(Intent.ACTION_POWER_CONNECTED)
178 | addAction(Intent.ACTION_POWER_DISCONNECTED)
179 | }
180 | registerReceiver(powerConnectionReceiver, powerFilter)
181 | }
182 |
183 | override fun onDestroy() {
184 | try {
185 | mGM?.turnOff()
186 | mGM?.closeSession()
187 | }
188 | catch (e: GlyphException) {
189 | Log.e(TAG, e.message!!)
190 | }
191 | mGM?.unInit()
192 |
193 | unregisterReceiver(powerConnectionReceiver)
194 | unregisterSensorListener()
195 |
196 | super.onDestroy()
197 | }
198 |
199 | private fun init() {
200 | mCallback = object : GlyphManager.Callback {
201 | override fun onServiceConnected(componentName: ComponentName) {
202 | if (Common.is20111()) mGM?.register(Common.DEVICE_20111)
203 | if (Common.is22111()) mGM?.register(Common.DEVICE_22111)
204 | if (Common.is23111()) mGM?.register(Common.DEVICE_23111)
205 | }
206 |
207 | override fun onServiceDisconnected(componentName: ComponentName) {
208 | mGM?.turnOff()
209 | mGM?.closeSession()
210 | }
211 | }
212 | }
213 |
214 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
215 |
216 | if(intent != null) {
217 | val action = intent.action
218 | if(action == "POWER_ON") {
219 | registerSensorListener()
220 | }
221 | else if(action == "POWER_OFF") {
222 | mGM?.turnOff()
223 | mGM?.closeSession()
224 | unregisterSensorListener()
225 | signalEE()
226 | }
227 | else {
228 | val notification: Notification = Notification.Builder(this, CHANNEL_ID)
229 | .setContentTitle(this.getString(R.string.title_foreground_notification))
230 | .setOngoing(true)
231 | .setSmallIcon(R.drawable.ic_sunlight)
232 | .build()
233 |
234 | startForeground(2, notification)
235 | }
236 | }
237 |
238 | return START_STICKY
239 | }
240 |
241 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/batteryindicator/PowerConnectionReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.batteryindicator
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.BroadcastReceiver
6 |
7 | class PowerConnectionReceiver: BroadcastReceiver() {
8 | override fun onReceive(context: Context, intent: Intent) {
9 | val action = intent.action
10 |
11 | if(action == Intent.ACTION_POWER_CONNECTED) {
12 | val serviceIntent = Intent(context, BatteryIndicatorService::class.java)
13 | serviceIntent.action = "POWER_ON"
14 | context.startService(serviceIntent)
15 | }
16 | else if(action == Intent.ACTION_POWER_DISCONNECTED) {
17 | val serviceIntent = Intent(context, BatteryIndicatorService::class.java)
18 | serviceIntent.action = "POWER_OFF"
19 | context.startService(serviceIntent)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/composer/BeatDetector.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.composer
2 | import android.content.Context
3 | import com.chaquo.python.PyObject
4 | import com.chaquo.python.Python
5 | import com.chaquo.python.android.AndroidPlatform
6 |
7 | object BeatDetector {
8 |
9 | /**
10 | * Given a path to a wav file it returns a 2D array of beats: each beat is a
11 | * pair (timestamp, energy), beats are grouped into high-freq-right-ch, high-freq-low-ch,
12 | * low-freq-mono, high-freq-mono and low-freq-mono (yes, again)
13 | * @param context: app context
14 | * @param filepath: path to wav file in app's filesystem
15 | * @return 2D array containing the beats
16 | * */
17 | fun detectBeatsAndFrequencies(context: Context, filepath: String, filename: String):
18 | Pair, List>>> {
19 | // this method uses Chaquopy to execute python code and librosa which is a python library
20 | // that let us extract beats from a tune
21 | if (!Python.isStarted()) {
22 | Python.start(AndroidPlatform(context))
23 | }
24 |
25 | val python = Python.getInstance()
26 | val pythonModule = python.getModule("beatDetectorFun")
27 |
28 | val result: PyObject = pythonModule.callAttr("detect_beats_and_frequencies", filepath, filename)
29 |
30 | val tempos = listOf(result.asList()[0].toDouble(), result.asList()[1].toDouble())
31 |
32 | val rawBeats: MutableList>> = mutableListOf()
33 |
34 | for (band in result.asList()[2].asList()) {
35 | val beatsBand: MutableList> = mutableListOf()
36 | for (beat in band.asList()) {
37 | val time = beat.asList()[0].toInt()
38 | val energy = beat.asList()[1].toDouble()
39 | beatsBand.add(Pair(time, energy))
40 | }
41 | rawBeats.add(beatsBand)
42 | }
43 |
44 | return Pair(tempos, rawBeats)
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/composer/FileHandling.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.composer
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.provider.OpenableColumns
7 | import android.util.Base64
8 | import android.webkit.MimeTypeMap
9 | import com.arthenica.ffmpegkit.FFprobeKit
10 | import com.frank.glyphify.R
11 | import java.io.ByteArrayOutputStream
12 | import java.io.File
13 | import java.util.Locale
14 | import java.util.zip.Deflater
15 | import java.util.zip.Inflater
16 |
17 | object FileHandling {
18 | fun getFileNameFromUri(context: Context, uri: Uri): String {
19 | val cursor = context.contentResolver.query(uri, null, null, null, null, null)
20 | var filename = ""
21 |
22 | try {
23 | if (cursor != null && cursor.moveToFirst()) {
24 | val filenameColumnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
25 | if(filenameColumnIndex != -1){
26 | filename = cursor.getString(filenameColumnIndex)
27 | }
28 | cursor.close()
29 | return filename
30 | }
31 | }
32 | catch (e: Exception) {
33 | return filename
34 | }
35 |
36 | return filename
37 | }
38 |
39 | fun getFileExtension(uri: Uri, contentResolver: ContentResolver): String {
40 | var extension: String? = null
41 |
42 | // check uri format to avoid null
43 | if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
44 | // the file is stored in the provider with a ContentProvider
45 | val mime = MimeTypeMap.getSingleton()
46 | extension = mime.getExtensionFromMimeType(contentResolver.getType(uri))
47 | }
48 | else {
49 | extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(uri.path)).toString())
50 | extension = extension?.lowercase(Locale.ROOT)
51 | }
52 |
53 | return extension ?: ""
54 | }
55 |
56 | /**
57 | * Retrieves details of a wav audio file using FFprobe
58 | * @param filePath: path of the wav file
59 | * @return duration and sample rate
60 | * @throws RuntimeException if something went wrong
61 | * */
62 | fun getAudioDetails(context: Context, filePath: String): Pair {
63 | try {
64 | val mediaInformation = FFprobeKit.getMediaInformation(filePath).mediaInformation
65 | val duration = mediaInformation.duration.toDouble()
66 | if(duration > 300) throw RuntimeException(context.getString(R.string.error_duration_long))
67 |
68 | val streams = mediaInformation.streams
69 | for (stream in streams) {
70 | if (stream.type == "audio") {
71 | val sampleRate = stream.sampleRate.toInt()
72 | return Pair(duration, sampleRate)
73 | }
74 | }
75 |
76 | throw RuntimeException(context.getString(R.string.error_invalid_audio_file))
77 | }
78 | catch (e: RuntimeException) {
79 | throw e
80 | }
81 | }
82 |
83 | /**
84 | * Compress data using zlib and then encodes it in base64
85 | * @param data: the data to work on
86 | * @return a string containing the base64 representation of the compresses data
87 | * */
88 | fun compressAndEncode(data: String): String {
89 | val input = data.toByteArray(Charsets.UTF_8)
90 |
91 | // compress the bytes
92 | val deflater = Deflater(Deflater.BEST_COMPRESSION)
93 | deflater.setInput(input)
94 | deflater.finish()
95 |
96 | val outputStream = ByteArrayOutputStream(input.size)
97 | val buffer = ByteArray(1024)
98 | while (!deflater.finished()) {
99 | val count = deflater.deflate(buffer) // compress data
100 | outputStream.write(buffer, 0, count)
101 | }
102 | outputStream.close()
103 | val compressedBytes = outputStream.toByteArray()
104 |
105 | // encode to Base64
106 | var base64Data = Base64.encodeToString(compressedBytes, Base64.DEFAULT)
107 |
108 | // remove padding bytes
109 | base64Data = base64Data.trimEnd('=')
110 |
111 | // add newline every 76 characters
112 | val formattedData = base64Data.chunked(76).joinToString("\n")
113 |
114 | return "$formattedData\n"
115 | }
116 |
117 | /**
118 | * Decodes a base64 string and then decompresses it using zlib
119 | * @param base64Data: the base64 encoded and compressed data
120 | * @return a string containing the original data
121 | */
122 | fun decodeAndDecompress(data: String): String {
123 | // Remove newline characters
124 | val base64Data = data.replace("\n", "")
125 |
126 | // Add padding bytes
127 | val padding = "=".repeat((4 - base64Data.length % 4) % 4)
128 | val paddedBase64Data = base64Data + padding
129 |
130 | // Decode from Base64
131 | val compressedBytes = Base64.decode(paddedBase64Data, Base64.DEFAULT)
132 |
133 | // Decompress the bytes
134 | val inflater = Inflater()
135 | inflater.setInput(compressedBytes)
136 |
137 | val outputStream = ByteArrayOutputStream(compressedBytes.size)
138 | val buffer = ByteArray(1024)
139 | while (!inflater.finished()) {
140 | val count = inflater.inflate(buffer)
141 | outputStream.write(buffer, 0, count)
142 | }
143 | outputStream.close()
144 | val decompressedBytes = outputStream.toByteArray()
145 |
146 | return String(decompressedBytes, Charsets.UTF_8)
147 | }
148 |
149 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/composer/LightEffects.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.composer
2 |
3 | import com.frank.glyphify.Constants.LIGHT_DURATION_US
4 | import kotlin.math.pow
5 |
6 | /**
7 | * A collection of light effects which are applied to a single beat
8 | */
9 | object LightEffects {
10 |
11 | private fun calculateBase(tempo: Double, base: Int, coeff: Double, range: IntRange): Int {
12 | return (base - coeff * tempo).toInt().coerceIn(range)
13 | }
14 |
15 |
16 | fun expDecay(beat: Pair, tempo: Double, offsetSlotsOut: Int = 0):
17 | List> {
18 | var (timestamp, lightIntensity) = beat
19 | val result = mutableListOf>()
20 |
21 | val slotsOut = calculateBase(tempo, 23, 7.0/60, 4..16)
22 |
23 | for (i in 2 downTo 1) {
24 | val newTs = if(timestamp - LIGHT_DURATION_US * i < 0) 0 else timestamp - LIGHT_DURATION_US * i
25 | val newIntensity = (lightIntensity * (2 - i).toDouble() / 2).toInt()
26 | result.add(Pair(newTs, newIntensity))
27 | }
28 |
29 | result.add(Pair(timestamp, lightIntensity))
30 |
31 | for (i in 1..slotsOut) {
32 | timestamp += LIGHT_DURATION_US
33 | val newIntensity = (lightIntensity * (1 - (i.toDouble() / slotsOut).pow(2))).toInt()
34 | result.add(Pair(timestamp, newIntensity))
35 | }
36 |
37 | return result
38 | }
39 |
40 | fun fastBlink(beat: Pair, tempo: Double, offsetSlotsOut: Int = 0):
41 | List> {
42 | var (timestamp, lightIntensity) = beat
43 | val result = mutableListOf>()
44 |
45 | result.add(Pair(timestamp, lightIntensity))
46 |
47 | val slotsOut = calculateBase(tempo, 11, 1.0/20, 4..8)
48 |
49 | for (i in 1..slotsOut) {
50 | timestamp += LIGHT_DURATION_US
51 | val newIntensity = (lightIntensity * (slotsOut - i).toDouble() / slotsOut).toInt()
52 | result.add(Pair(timestamp, newIntensity))
53 | }
54 |
55 | return result
56 | }
57 |
58 | fun circusTent(beat: Pair, tempo: Double, offsetSlotsIn: Int = 0):
59 | List> {
60 | var (timestamp, lightIntensity) = beat
61 | val tmp = mutableListOf>()
62 |
63 | fun calculateIntensity(i: Int, slots: Int): Int {
64 | return (lightIntensity * (1 - (i.toDouble() / slots).pow(2))).toInt()
65 | }
66 |
67 |
68 | val slotsIn = calculateBase(tempo, 14, 1.0/15, 4..10)
69 |
70 | for (i in slotsIn downTo 1) {
71 | val newTs = if(timestamp - LIGHT_DURATION_US * i < 0) 0 else timestamp - LIGHT_DURATION_US * i
72 | tmp.add(Pair(newTs, calculateIntensity(i, slotsIn)))
73 | }
74 |
75 | tmp.add(Pair(timestamp, lightIntensity))
76 |
77 | val decreaseSlots = slotsIn * 2 / 3
78 | for (i in 1..decreaseSlots) {
79 | timestamp += LIGHT_DURATION_US
80 | tmp.add(Pair(timestamp, calculateIntensity(i, slotsIn)))
81 | }
82 |
83 | // mirror the light effect to have a symmetrical descent
84 | val result = mutableListOf>()
85 | result.addAll(tmp)
86 | tmp.reverse()
87 |
88 | for ((t, l) in tmp) {
89 | timestamp += LIGHT_DURATION_US
90 | result.add(Pair(timestamp, l))
91 | }
92 |
93 | return result
94 | }
95 |
96 | fun flickering(beat: Pair, tempo: Double, numFlickers: Int,
97 | offsetSlotsIn: Int = 0): List> {
98 | var (timestamp, lightIntensity) = beat
99 | val result = mutableListOf>()
100 |
101 | val slotsOut = calculateBase(tempo, 11, 1.0/20, 4..8)
102 |
103 | for(i in 1..numFlickers) {
104 | result.addAll(fastBlink(Pair(timestamp, lightIntensity), tempo))
105 | timestamp += LIGHT_DURATION_US * (slotsOut + 4)
106 | }
107 |
108 | return result
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/extendedessential/Animations.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.extendedessential
2 |
3 | import com.nothing.ketchum.Common
4 | import com.nothing.ketchum.GlyphFrame
5 | import kotlin.math.ceil
6 | import kotlin.math.floor
7 |
8 | object Animations {
9 |
10 | fun pulseAnimation(frame: GlyphFrame.Builder, glyphs: List, time: Long, intensity: Int,
11 | stepSize: Int, perStepDelay: Long): GlyphFrame.Builder {
12 |
13 | var dynamicFrame = frame
14 | val cycleDuration = 5000L // 5000ms cycle duration to ensure 4000ms between animations
15 |
16 | val currentTick = time % cycleDuration
17 |
18 | val light = maxOf(intensity - stepSize * (currentTick / perStepDelay).toInt(), 0)
19 | for (glyph in glyphs) {
20 | dynamicFrame = dynamicFrame.buildChannel(glyph, light)
21 | }
22 |
23 | return dynamicFrame
24 | }
25 |
26 | private fun getVariableGlyphZones(glyphs: List): List> {
27 | val variableGlyphZones = MutableList(2) { mutableListOf() }
28 | if(Common.is22111()) { // phone2 has 2 variable glyphs
29 | val (zone1, zone2) = glyphs.partition { it <= 18 }
30 | variableGlyphZones[0] = zone1.toMutableList()
31 | variableGlyphZones[1] = zone2.toMutableList()
32 | }
33 | // (1) and (2a) only have one variable glyph zone
34 | else variableGlyphZones[0] = glyphs.toMutableList()
35 |
36 | return variableGlyphZones
37 | }
38 |
39 | fun pingPongAnimation(frame: GlyphFrame.Builder, glyphs: List, time: Long, intensity: Int,
40 | perStepDelay: Long): GlyphFrame.Builder {
41 |
42 | val variableGlyphZones = getVariableGlyphZones(glyphs)
43 |
44 | val speed = perStepDelay * 2
45 | var dynamicFrame = frame
46 | var polarGlyphIndex: Int
47 | var revertAnim: Boolean
48 | val baseIntensity = 0
49 |
50 | for(variableZone in variableGlyphZones) {
51 | val animDuration = variableZone.size * speed
52 | if(animDuration == 0L) continue
53 |
54 | val elapsedTick = ((time / speed) % variableZone.size).toInt()
55 |
56 | if((time / animDuration) % 2L == 0L) {
57 | revertAnim = false
58 | polarGlyphIndex = variableZone[0] + elapsedTick
59 | }
60 | else {
61 | revertAnim = true
62 | polarGlyphIndex = variableZone[0] + (variableZone.size - 1) - elapsedTick
63 | }
64 |
65 | for(glyph in variableZone) {
66 | dynamicFrame = dynamicFrame.buildChannel(glyph, baseIntensity)
67 | }
68 |
69 | var currIntensity = intensity
70 | if(!revertAnim) {
71 | for(i in polarGlyphIndex downTo maxOf(polarGlyphIndex - 2, variableZone[0])) {
72 | dynamicFrame.buildChannel(i, currIntensity)
73 | currIntensity -= intensity / 2
74 | currIntensity.coerceAtLeast(baseIntensity)
75 | }
76 | }
77 | else {
78 | for(i in polarGlyphIndex .. minOf(polarGlyphIndex + 2, variableZone.last())) {
79 | dynamicFrame.buildChannel(i, currIntensity)
80 | currIntensity -= intensity / 2
81 | currIntensity.coerceAtLeast(baseIntensity)
82 | }
83 | }
84 |
85 | }
86 |
87 | return dynamicFrame
88 | }
89 |
90 | fun expansionAnimation(frame: GlyphFrame.Builder, glyphs: List, time: Long, intensity: Int,
91 | perStepDelay: Long): GlyphFrame.Builder {
92 |
93 | val speed = perStepDelay * 2
94 | val variableGlyphZones = getVariableGlyphZones(glyphs)
95 | var dynamicFrame = frame
96 | var currIntensity: Int
97 | var phase2: Boolean
98 |
99 | for(variableZone in variableGlyphZones) {
100 | if(variableZone.isEmpty()) continue
101 | val glyphSize = variableZone.size
102 | val animDuration = glyphSize * speed
103 |
104 | val middle = (variableZone.first() + variableZone.last()) / 2.0
105 | val middlePoints = listOf(floor(middle).toInt(), ceil(middle).toInt())
106 |
107 | var elapsedTick = ((time / speed) % glyphSize).toInt()
108 |
109 | if((time / animDuration) % 2L == 0L) {
110 | phase2 = false
111 | currIntensity = intensity
112 | }
113 | else {
114 | phase2 = true
115 | currIntensity = 0
116 | }
117 |
118 | if(elapsedTick < glyphSize / 4 && !phase2) {
119 | currIntensity = (elapsedTick * intensity * 4 / glyphSize).coerceAtMost(intensity)
120 | dynamicFrame.buildChannel(middlePoints.first(), currIntensity)
121 | dynamicFrame.buildChannel(middlePoints.last(), currIntensity)
122 | continue
123 | }
124 | else if(elapsedTick >= glyphSize * 3 / 4 && phase2) {
125 | if(elapsedTick == glyphSize - 1) currIntensity = 0
126 | else currIntensity = (intensity * (1 - elapsedTick * 1.0 / glyphSize)).toInt()
127 |
128 | for(index in variableZone.first() + 1..< variableZone.last()) {
129 | dynamicFrame.buildChannel(index, 0)
130 | }
131 | dynamicFrame.buildChannel(variableZone.first(), currIntensity)
132 | dynamicFrame.buildChannel(variableZone.last(), currIntensity)
133 | continue
134 | }
135 |
136 | for(index in middlePoints.first() downTo middlePoints.first() - elapsedTick / 2) {
137 | dynamicFrame.buildChannel(index, currIntensity)
138 | }
139 |
140 | for(index in middlePoints.last()..middlePoints.last() + elapsedTick / 2) {
141 | dynamicFrame.buildChannel(index, currIntensity)
142 | }
143 |
144 | }
145 |
146 | return dynamicFrame
147 | }
148 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/extendedessential/ExtendedEssentialReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.extendedessential
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class ExtendedEssentialReceiver : BroadcastReceiver() {
8 | private var isPhoneLocked = false
9 |
10 | override fun onReceive(context: Context, intent: Intent) {
11 | val serviceIntent = Intent(context, ExtendedEssentialService::class.java)
12 |
13 | when (intent.action) {
14 | Intent.ACTION_SCREEN_OFF -> {
15 | isPhoneLocked = true
16 | serviceIntent.action = "PHONE_LOCKED"
17 | context.startService(serviceIntent)
18 | }
19 | Intent.ACTION_USER_PRESENT -> {
20 | isPhoneLocked = false
21 | serviceIntent.action = "PHONE_UNLOCKED"
22 | context.startService(serviceIntent)
23 | }
24 | }
25 | }
26 |
27 | fun isPhoneLocked() = isPhoneLocked
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/glyph/visualizer/GlyphVisualizer.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.glyph.visualizer
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.media.MediaMetadataRetriever
6 | import android.media.MediaPlayer
7 | import com.frank.glyphify.glyph.composer.FileHandling.decodeAndDecompress
8 | import com.nothing.ketchum.Common
9 | import com.nothing.ketchum.Glyph
10 | import com.nothing.ketchum.GlyphException
11 | import com.nothing.ketchum.GlyphManager
12 |
13 | class GlyphVisualizer(private var context: Context): AutoCloseable {
14 | private var mGM: GlyphManager? = null
15 | private var mCallback: GlyphManager.Callback? = null
16 |
17 | private var numZones: Int = 5
18 | private var exitFlag: Boolean = false
19 |
20 | init {
21 | init()
22 | val localGM = GlyphManager.getInstance(context)
23 | localGM?.init(mCallback)
24 | mGM = localGM
25 | }
26 |
27 | private fun init() {
28 | mCallback = object : GlyphManager.Callback {
29 | override fun onServiceConnected(componentName: ComponentName) {
30 | if (Common.is20111()) mGM?.register(Common.DEVICE_20111)
31 | if (Common.is22111()) mGM?.register(Common.DEVICE_22111)
32 | if (Common.is23111()) mGM?.register(Common.DEVICE_23111)
33 | }
34 |
35 | override fun onServiceDisconnected(componentName: ComponentName) {
36 | mGM?.turnOff()
37 | mGM?.closeSession()
38 | }
39 | }
40 | }
41 |
42 | private fun getGlyphMapping(index: Int,): List {
43 | var glyphIndexes: List
44 | when(index) {
45 | 0 -> {
46 | if(Common.is22111()) {
47 | glyphIndexes = (Glyph.Code_22111.A1..Glyph.Code_22111.A2).toList()
48 | }
49 | else glyphIndexes = listOf(index)
50 | }
51 | 1 -> {
52 | if(Common.is22111()) {
53 | glyphIndexes = listOf(Glyph.Code_22111.B1)
54 | }
55 | else glyphIndexes = listOf(index)
56 | }
57 | 2 -> {
58 | if(Common.is20111()) {
59 | glyphIndexes = (Glyph.Code_20111.C1..Glyph.Code_20111.C4).toList()
60 | }
61 | else if(Common.is22111()) {
62 | glyphIndexes = (Glyph.Code_22111.C1_1..Glyph.Code_22111.C6).toList()
63 | }
64 | else glyphIndexes = listOf(index)
65 | }
66 | 3 -> {
67 | if(Common.is20111()) {
68 | glyphIndexes = (Glyph.Code_20111.D1_1..Glyph.Code_20111.D1_8).toList()
69 | }
70 | else if(Common.is22111()) {
71 | glyphIndexes = (Glyph.Code_22111.D1_1..Glyph.Code_22111.D1_8).toList()
72 | }
73 | else glyphIndexes = listOf(index)
74 | }
75 | 4 -> {
76 | if(Common.is20111()) {
77 | glyphIndexes = listOf(Glyph.Code_20111.E1)
78 | }
79 | else if(Common.is22111()) {
80 | glyphIndexes = listOf(Glyph.Code_22111.E1)
81 | }
82 | else glyphIndexes = listOf(index)
83 | }
84 | else -> glyphIndexes = listOf(index)
85 | }
86 |
87 | return glyphIndexes
88 | }
89 |
90 | private fun getAuthorDataFromFile(filePath: String): String {
91 | val retriever = MediaMetadataRetriever()
92 | retriever.setDataSource(filePath)
93 | val commpressedEncodedAuthorTag = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_AUTHOR) ?: "Unknown"
94 | retriever.release()
95 |
96 | return decodeAndDecompress(commpressedEncodedAuthorTag)
97 | }
98 |
99 | fun startVisualization(filePath: String, onFinished: () -> Unit) {
100 | val beats = getAuthorDataFromFile(filePath)
101 | .trim()
102 | .lines()
103 | .map { line ->
104 | line.split(",")
105 | .filter { it.isNotEmpty() }
106 | .map { it.toInt() }
107 | .toIntArray()
108 | }
109 |
110 | if(beats.isEmpty()) return
111 | numZones = beats[0].size
112 |
113 | exitFlag = false
114 | Thread {
115 | var mediaPlayer: MediaPlayer? = null
116 | val sleepTime = 16_666_000L // 16.666 milliseconds
117 |
118 | mGM?.openSession()
119 | for (beat in beats) {
120 | val frame = mGM!!.glyphFrameBuilder
121 |
122 | for ((zone, intensity) in beat.withIndex()) {
123 | if (intensity != 0) {
124 | if(numZones == 5) {
125 | val glyphs = getGlyphMapping(zone)
126 | for (glyph in glyphs) frame.buildChannel(glyph, intensity)
127 | }
128 | else frame.buildChannel(zone, intensity)
129 | }
130 | }
131 |
132 | mGM!!.toggle(frame.build())
133 |
134 | if (mediaPlayer == null) {
135 | mediaPlayer = MediaPlayer().apply {
136 | setDataSource(filePath)
137 | prepare()
138 | start()
139 | }
140 | }
141 |
142 | val startTime = System.nanoTime()
143 |
144 | while (System.nanoTime() - startTime < sleepTime) {
145 | // busy-wait loop to achieve precise timing
146 | }
147 |
148 | if (exitFlag) {
149 | mGM?.turnOff()
150 | mGM?.closeSession()
151 | mediaPlayer.stop()
152 | mediaPlayer.release()
153 | onFinished()
154 | return@Thread
155 | }
156 | }
157 |
158 | mGM?.turnOff()
159 | mGM?.closeSession()
160 | mediaPlayer?.stop()
161 | mediaPlayer?.release()
162 | onFinished()
163 | }.start()
164 | }
165 |
166 | fun stopVisualization() {
167 | exitFlag = true
168 | }
169 |
170 | override fun close() {
171 | try {
172 | mGM?.turnOff()
173 | mGM?.closeSession()
174 | }
175 | catch (e: GlyphException) {
176 | e.printStackTrace()
177 | }
178 | mGM?.unInit()
179 | }
180 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/ui/dialogs/Dialog.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.ui.dialogs
2 |
3 | import android.Manifest
4 | import android.app.AlertDialog
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.graphics.Color
8 | import android.graphics.drawable.ColorDrawable
9 | import android.net.Uri
10 | import android.os.Handler
11 | import android.os.Looper
12 | import android.text.Editable
13 | import android.text.TextWatcher
14 | import android.view.LayoutInflater
15 | import android.view.View
16 | import android.widget.Button
17 | import android.widget.EditText
18 | import android.widget.TextView
19 | import com.frank.glyphify.PermissionHandling
20 | import com.frank.glyphify.R
21 |
22 | object Dialog {
23 | fun showDialog(
24 | context: Context,
25 | layoutId: Int,
26 | buttonActions: Map Unit>,
27 | isCancelable: Boolean = true,
28 | delayEnableButtonId: Int? = null,
29 | delayMillis: Long = 0,
30 | onDismiss: (() -> Unit)? = null,
31 | errorMsg: String? = null,
32 | ) {
33 | val dialogView = LayoutInflater.from(context).inflate(layoutId, null)
34 | val dialog = AlertDialog.Builder(context)
35 | .setView(dialogView)
36 | .setCancelable(isCancelable)
37 | .create()
38 |
39 | for ((buttonId, action) in buttonActions) {
40 | val button = dialogView.findViewById(buttonId)
41 | button.setOnClickListener {
42 | action(dialogView)
43 | dialog.dismiss()
44 | }
45 | button.backgroundTintList = null
46 | }
47 |
48 | if(errorMsg != null) {
49 | val errorMessage = dialogView.findViewById(R.id.error_message)
50 | if(errorMessage != null) {
51 | errorMessage.text = errorMsg
52 | }
53 | }
54 |
55 | val editText = dialogView.findViewById(R.id.editText)
56 | if(editText != null) {
57 | dialogView.findViewById(R.id.positiveButton).isEnabled = false
58 | editText.addTextChangedListener(object : TextWatcher {
59 | override fun afterTextChanged(s: Editable?) {}
60 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
61 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
62 | if (s != null) {
63 | dialogView.findViewById(R.id.positiveButton).isEnabled = s.isNotBlank()
64 | }
65 | }
66 | })
67 | }
68 |
69 | delayEnableButtonId?.let {
70 | dialogView.findViewById(it).isEnabled = false
71 | Handler(Looper.getMainLooper()).postDelayed({
72 | dialogView.findViewById(it).isEnabled = true
73 | }, delayMillis)
74 | }
75 |
76 | dialog.setOnDismissListener {
77 | onDismiss?.invoke()
78 | }
79 |
80 | dialog.show()
81 | dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
82 | }
83 |
84 | fun supportMe(context: Context, permHandler: PermissionHandling) {
85 | showDialog(
86 | context,
87 | R.layout.first_boot,
88 | mapOf(
89 | R.id.paypalBtn to {
90 | context.startActivity(
91 | Intent(
92 | Intent.ACTION_VIEW,
93 | Uri.parse("https://www.paypal.com/donate/?hosted_button_id=HJU8Y7F34Z6TL"))
94 | )
95 | },
96 | R.id.negativeButton to {
97 | val permissions = mutableListOf(
98 | Manifest.permission.POST_NOTIFICATIONS
99 | )
100 | permHandler.askRequiredPermissions(permissions, R.layout.dialog_perm_notifications)
101 | }
102 | ),
103 | isCancelable = false,
104 | delayEnableButtonId = R.id.negativeButton,
105 | delayMillis = 10000,
106 | onDismiss = {}
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/ui/notifications/AppsChoiceFragment.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.ui.notifications
2 |
3 | import android.content.Intent
4 | import android.content.pm.ApplicationInfo
5 | import android.content.res.Resources
6 | import android.graphics.Bitmap
7 | import android.graphics.Canvas
8 | import android.graphics.drawable.BitmapDrawable
9 | import android.graphics.drawable.Drawable
10 | import android.os.Bundle
11 | import android.util.Log
12 | import android.view.LayoutInflater
13 | import android.view.Menu
14 | import android.view.MenuItem
15 | import android.view.View
16 | import android.view.ViewGroup
17 | import android.widget.LinearLayout
18 | import android.widget.TextView
19 | import androidx.core.app.NotificationManagerCompat
20 | import androidx.fragment.app.Fragment
21 | import androidx.fragment.app.setFragmentResult
22 | import androidx.navigation.fragment.findNavController
23 | import androidx.recyclerview.widget.LinearLayoutManager
24 | import androidx.recyclerview.widget.RecyclerView
25 | import com.frank.glyphify.R
26 | import com.frank.glyphify.Util.resizeDrawable
27 | import com.frank.glyphify.databinding.FragmentAppsChoiceBinding
28 | import com.frank.glyphify.databinding.FragmentContactsChoiceBinding
29 | import com.google.android.material.bottomnavigation.BottomNavigationView
30 | import com.google.android.material.button.MaterialButton
31 |
32 | class AppsChoiceFragment: Fragment() {
33 |
34 | private var _binding: FragmentContactsChoiceBinding? = null
35 |
36 | private var appDetails: List> = emptyList()
37 | private lateinit var appsAdapter: AppsChoiceFragment.AppsAdapter
38 |
39 | override fun onCreateView(
40 | inflater: LayoutInflater,
41 | container: ViewGroup?,
42 | savedInstanceState: Bundle?
43 | ): View {
44 |
45 | // get installed apps list
46 | val intent = Intent(Intent.ACTION_MAIN, null)
47 | intent.addCategory(Intent.CATEGORY_LAUNCHER)
48 | val appList = requireContext().packageManager.queryIntentActivities(intent, 0)
49 |
50 | // filter out apps not using notifications
51 | val notificationManager = NotificationManagerCompat.from(requireContext())
52 | val currentPackageName = requireContext().packageName
53 |
54 | val appsWithNotifications = appList.filter { resolveInfo ->
55 | val appInfo = resolveInfo.activityInfo.applicationInfo
56 | notificationManager.areNotificationsEnabled() &&
57 | appInfo.packageName != currentPackageName
58 | }
59 |
60 | // get package name, app name and icon for each app
61 | val packageManager = requireContext().packageManager
62 | appDetails = appsWithNotifications.map { resolveInfo ->
63 | val appInfo = resolveInfo.activityInfo.applicationInfo
64 |
65 | val packageName = appInfo.packageName
66 | val appName = packageManager.getApplicationLabel(appInfo).toString()
67 | val icon = packageManager.getApplicationIcon(appInfo)
68 |
69 | Triple(packageName, appName, icon)
70 | }.sortedBy { it.second }
71 |
72 | val binding = FragmentAppsChoiceBinding.inflate(inflater, container, false)
73 |
74 | appsAdapter = AppsAdapter()
75 | binding.recyclerViewApps.adapter = appsAdapter
76 | binding.recyclerViewApps.layoutManager = LinearLayoutManager(requireContext())
77 |
78 | return binding.root
79 | }
80 |
81 | inner class AppsAdapter(): RecyclerView.Adapter() {
82 |
83 | inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
84 | val btnChooseApp: MaterialButton = view.findViewById(R.id.btn_item)
85 | }
86 |
87 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
88 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_rr_centered_btn, parent, false)
89 | return ViewHolder(view)
90 | }
91 |
92 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
93 | holder.btnChooseApp.text = appDetails[position].second
94 | val resizedIcon = resizeDrawable(resources, appDetails[position].third, 64, 64)
95 | holder.btnChooseApp.setCompoundDrawablesWithIntrinsicBounds(
96 | resizedIcon,
97 | null,
98 | null,
99 | null
100 | )
101 |
102 | holder.btnChooseApp.setOnClickListener {
103 | val result = Bundle().apply {
104 | putString("chosen_package_name", appDetails[position].first)
105 | }
106 | setFragmentResult("app_choice", result)
107 | findNavController().popBackStack()
108 | }
109 | }
110 |
111 | override fun getItemCount() = appDetails.size
112 | }
113 |
114 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
115 | super.onViewCreated(view, savedInstanceState)
116 | val btnBack = requireActivity().findViewById(R.id.toolbar_btn_back)
117 | btnBack.setOnClickListener {
118 | findNavController().popBackStack()
119 | }
120 | }
121 |
122 | override fun onResume() {
123 | super.onResume()
124 | val activity = requireActivity()
125 | activity.findViewById(R.id.toolbar_btn_back).visibility = View.VISIBLE
126 | activity.findViewById(R.id.toolbar_app_name).visibility = View.GONE
127 | activity.findViewById(R.id.toolbar_btns_wrapper).visibility = View.GONE
128 |
129 | val bottomNavigationView: BottomNavigationView = requireActivity().findViewById(R.id.nav_view)
130 | val menu: Menu = bottomNavigationView.menu
131 | val menuItem: MenuItem = menu.getItem(0)
132 | menuItem.isChecked = true
133 | }
134 |
135 | override fun onDestroyView() {
136 | super.onDestroyView()
137 | _binding = null
138 | }
139 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/ui/notifications/NotificationsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.ui.notifications
2 |
3 | import android.app.AlertDialog
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.SharedPreferences
7 | import android.content.res.ColorStateList
8 | import android.graphics.Color
9 | import android.graphics.drawable.ColorDrawable
10 | import android.os.Build
11 | import android.os.Bundle
12 | import android.provider.Settings
13 | import android.view.LayoutInflater
14 | import android.view.View
15 | import android.view.ViewGroup
16 | import android.widget.LinearLayout
17 | import android.widget.TextView
18 | import androidx.core.app.NotificationManagerCompat
19 | import androidx.core.content.ContextCompat
20 | import androidx.fragment.app.Fragment
21 | import androidx.navigation.NavOptions
22 | import androidx.navigation.fragment.findNavController
23 | import com.frank.glyphify.Constants.DIMMING_VALUES
24 | import com.frank.glyphify.Constants.PHONE1_MODEL_ID
25 | import com.frank.glyphify.Constants.PHONE2A_MODEL_ID
26 | import com.frank.glyphify.PermissionHandling
27 | import com.frank.glyphify.R
28 | import com.frank.glyphify.Util.exactAlarm
29 | import com.frank.glyphify.Util.loadPreferences
30 | import com.frank.glyphify.databinding.FragmentHomeBinding
31 | import com.frank.glyphify.databinding.FragmentNotifications1Binding
32 | import com.frank.glyphify.databinding.FragmentNotifications2Binding
33 | import com.frank.glyphify.databinding.FragmentNotifications2aBinding
34 | import com.frank.glyphify.glyph.extendedessential.ExtendedEssentialService
35 | import com.frank.glyphify.ui.dialogs.Dialog
36 | import com.google.android.material.button.MaterialButton
37 | import com.google.android.material.button.MaterialButtonToggleGroup
38 | import java.math.BigInteger
39 |
40 |
41 | class NotificationsFragment: Fragment() {
42 |
43 | private var _binding: FragmentHomeBinding? = null
44 | private val binding get() = _binding!!
45 | private var numZones: Int = 11
46 | private lateinit var glyphsMapping: MutableList, Int>>
47 | private lateinit var permHandler: PermissionHandling
48 | private lateinit var sharedPref: SharedPreferences
49 |
50 | private fun loadContactsMapping() {
51 | val buttonContainer: ViewGroup = requireView().findViewById(R.id.buttonContainer)
52 |
53 | for (i in 0 until buttonContainer.childCount) {
54 | val child = buttonContainer.getChildAt(i)
55 |
56 | if (child is MaterialButton) {
57 | val mappingTriple = glyphsMapping[i]
58 | when {
59 | mappingTriple.second.size == 1 ->
60 | child.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_single)
61 | mappingTriple.second.size > 1 ->
62 | child.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_apps)
63 | else ->
64 | child.icon = null
65 | }
66 |
67 | val glyphBtn = resources.getResourceEntryName(child.id).split("_")
68 | val glyphId = glyphBtn[3].toInt()
69 |
70 | child.setOnClickListener {
71 | val bundle = Bundle().apply {
72 | putInt("zoneIndex", i)
73 | putInt("glyphId", glyphId)
74 | }
75 | val navOptions = NavOptions.Builder()
76 | .setEnterAnim(R.anim.fade_in)
77 | .setExitAnim(R.anim.fade_out)
78 | .setPopEnterAnim(R.anim.fade_in)
79 | .setPopExitAnim(R.anim.fade_out)
80 | .build()
81 | findNavController().navigate(R.id.navigation_contacts_choice, bundle, navOptions)
82 | }
83 | }
84 |
85 | }
86 | }
87 |
88 | private fun isNotificationServiceEnabled(): Boolean {
89 | val packageNames = NotificationManagerCompat.getEnabledListenerPackages(requireActivity())
90 | return packageNames.contains(requireContext().packageName)
91 | }
92 |
93 | private fun showDisclaimer() {
94 | Dialog.showDialog(
95 | requireContext(),
96 | R.layout.dialog_notification_listener_disclaimer,
97 | mapOf(
98 | R.id.positiveButton to {
99 | val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
100 | startActivity(intent)
101 | },
102 | R.id.negativeButton to { parentFragmentManager.popBackStack() }
103 | ),
104 | onDismiss = {
105 | loadContactsMapping()
106 | }
107 | )
108 | }
109 |
110 | private fun showSleepModeDialog() {
111 | val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_sleep_mode, null)
112 | val dialog = AlertDialog.Builder(context)
113 | .setView(dialogView)
114 | .create()
115 |
116 | val btnSetStartTime = dialogView.findViewById(R.id.btn_set_start_time)
117 | val btnSetEndTime = dialogView.findViewById(R.id.btn_set_end_time)
118 |
119 | val currStartTime = sharedPref.getString("sleepStart", "")
120 | val currEndTime = sharedPref.getString("sleepEnd", "")
121 |
122 | btnSetStartTime.text = getString(R.string.btn_sleep_mode_start) + ": " + currStartTime
123 | btnSetEndTime.text = getString(R.string.btn_sleep_mode_end) + ": " + currEndTime
124 |
125 | btnSetStartTime.setOnClickListener {
126 | TimePickerFragment(requireContext(), "sleepStart", btnSetStartTime).show(requireActivity().supportFragmentManager, "sleepMode")
127 | }
128 |
129 | btnSetEndTime.setOnClickListener {
130 | TimePickerFragment(requireContext(), "sleepEnd", btnSetEndTime).show(requireActivity().supportFragmentManager, "sleepMode")
131 | }
132 |
133 | dialog.show()
134 | dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
135 | }
136 |
137 | private fun setBtnSleepModeColor(bnt: MaterialButton, isSleepModeEnabled: Boolean) {
138 | val colorId = if(isSleepModeEnabled) R.color.red else R.color.black_russian
139 | bnt.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorId))
140 | }
141 |
142 | override fun onCreateView(
143 | inflater: LayoutInflater,
144 | container: ViewGroup?,
145 | savedInstanceState: Bundle?
146 | ): View {
147 |
148 | permHandler = PermissionHandling(requireActivity())
149 |
150 | val root = when(Build.MODEL) {
151 | PHONE1_MODEL_ID -> {
152 | numZones = 5
153 | val binding = FragmentNotifications1Binding.inflate(inflater, container, false)
154 | binding.root
155 | }
156 | PHONE2A_MODEL_ID -> {
157 | numZones = 3
158 | val binding = FragmentNotifications2aBinding.inflate(inflater, container, false)
159 | binding.root
160 | }
161 | else -> {
162 | numZones = 11
163 | val binding = FragmentNotifications2Binding.inflate(inflater, container, false)
164 | binding.root
165 | }
166 | }
167 |
168 | glyphsMapping = loadPreferences(requireContext(), numZones)
169 |
170 | return root
171 | }
172 |
173 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
174 | super.onViewCreated(view, savedInstanceState)
175 |
176 | if(!isNotificationServiceEnabled()) {
177 | showDisclaimer()
178 | }
179 | else loadContactsMapping()
180 |
181 | sharedPref = requireContext().getSharedPreferences("settings", Context.MODE_PRIVATE)
182 |
183 | val dimmingToggle = requireView().findViewById(R.id.dimming_toggle)
184 | val lastSelectionToggleId = sharedPref.getInt("dimming_ee_toggle_id", R.id.dimming_toggle_mid)
185 |
186 | dimmingToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
187 | if (isChecked) {
188 | val editor: SharedPreferences.Editor = sharedPref.edit()
189 | editor.putInt("dimming_ee_toggle_id", checkedId)
190 | editor.apply()
191 |
192 | val index = dimmingToggle.indexOfChild(dimmingToggle.findViewById(checkedId))
193 | val intent = Intent(activity, ExtendedEssentialService::class.java).apply {
194 | action = "UPDATE_INTENSITY"
195 | putExtra("intensity", DIMMING_VALUES[index])
196 | }
197 | requireActivity().startService(intent)
198 | }
199 | }
200 |
201 | dimmingToggle.check(lastSelectionToggleId)
202 |
203 | val btnSleepMode = requireView().findViewById(R.id.btn_sleep_mode)
204 | val isSleepModeActive = sharedPref.getBoolean("isSleepModeActive", false)
205 | setBtnSleepModeColor(btnSleepMode, isSleepModeActive)
206 |
207 | btnSleepMode.setOnClickListener {
208 | val editor: SharedPreferences.Editor = sharedPref.edit()
209 | editor.putBoolean("isSleepModeActive", true)
210 | editor.apply()
211 |
212 | setBtnSleepModeColor(btnSleepMode, true)
213 | showSleepModeDialog()
214 | }
215 |
216 | btnSleepMode.setOnLongClickListener {
217 | exactAlarm(requireContext(), "SLEEP_ON", 0)
218 | exactAlarm(requireContext(), "SLEEP_OFF", 0)
219 |
220 | val editor: SharedPreferences.Editor = sharedPref.edit()
221 | editor.putBoolean("isSleepModeActive", false)
222 | editor.apply()
223 |
224 | setBtnSleepModeColor(btnSleepMode, false)
225 | true
226 | }
227 | }
228 |
229 | override fun onResume() {
230 | super.onResume()
231 | val activity = requireActivity()
232 | activity.findViewById(R.id.toolbar_btn_back).visibility = View.GONE
233 | activity.findViewById(R.id.toolbar_app_name).visibility = View.VISIBLE
234 | activity.findViewById(R.id.toolbar_btns_wrapper).visibility = View.GONE
235 | }
236 |
237 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/frank/glyphify/ui/notifications/TimePickerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify.ui.notifications
2 |
3 | import android.app.Dialog
4 | import android.app.TimePickerDialog
5 | import android.content.Context
6 | import android.content.SharedPreferences
7 | import android.os.Bundle
8 | import android.text.format.DateFormat
9 | import android.widget.TimePicker
10 | import androidx.fragment.app.DialogFragment
11 | import com.frank.glyphify.R
12 | import com.frank.glyphify.Util.exactAlarm
13 | import com.google.android.material.button.MaterialButton
14 | import java.util.Calendar
15 |
16 | class TimePickerFragment(private val context: Context, private val key: String, private val btn: MaterialButton):
17 | DialogFragment(), TimePickerDialog.OnTimeSetListener {
18 |
19 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
20 | // Use the current time as the default values for the picker.
21 | val c = Calendar.getInstance()
22 | val hour = c.get(Calendar.HOUR_OF_DAY)
23 | val minute = c.get(Calendar.MINUTE)
24 |
25 | // Create a new instance of TimePickerDialog and return it.
26 | return TimePickerDialog(activity, TimePickerDialog.THEME_DEVICE_DEFAULT_DARK,
27 | this, hour, minute, DateFormat.is24HourFormat(activity))
28 | }
29 |
30 | override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
31 | val sharedPref: SharedPreferences =
32 | context.getSharedPreferences("settings", Context.MODE_PRIVATE)
33 |
34 | val time = "$hourOfDay:$minute"
35 | val editor: SharedPreferences.Editor = sharedPref.edit()
36 | editor.putString(key, time)
37 | editor.apply()
38 |
39 | if(key == "sleepStart") {
40 | btn.text = getString(R.string.btn_sleep_mode_start) + ": " + time
41 | exactAlarm(context, "SLEEP_ON", 1)
42 | }
43 | else if(key == "sleepEnd") {
44 | btn.text = getString(R.string.btn_sleep_mode_end) + ": " + time
45 | exactAlarm(context, "SLEEP_OFF", 1)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/python/beatDetectorFun.py:
--------------------------------------------------------------------------------
1 | import librosa
2 | import numpy as np
3 |
4 | def detect_beats_and_frequencies(filepath, filename):
5 | y, sr = librosa.load(filepath + "/" + filename)
6 |
7 | # compute STFT for mono and stereo signals
8 | stft = np.abs(librosa.stft(np.asfortranarray(y)))
9 |
10 | # low frequency band for mono signal
11 | low_band = stft[:stft.shape[0]//3, :]
12 |
13 | # high frequency band for mono signal
14 | high_band = stft[2 * stft.shape[0]//3:, :]
15 |
16 | all_beats = []
17 | tempos = []
18 | for band in [low_band, high_band]:
19 | # compute the onset strength for each band
20 | onset_env = librosa.onset.onset_strength(sr=sr, S=band)
21 |
22 | # detect beats in each band
23 | tempo, beat_frames = librosa.beat.beat_track(onset_envelope=onset_env, tightness=400, sr=sr)
24 | tempos.append(tempo)
25 |
26 | # convert beat frames to time in ms and round to nearest integer
27 | beat_times = np.round(librosa.frames_to_time(beat_frames, sr=sr) * 1000000).astype(int)
28 |
29 | # get the corresponding energy for each beat
30 | beat_energies = onset_env[beat_frames]
31 |
32 | # create a list of tuples (timestamp, energy) for each band
33 | beats_band = [(time, energy) for time, energy in zip(beat_times, beat_energies)]
34 |
35 | all_beats.append(beats_band)
36 |
37 | return tempos[0], tempos[1], all_beats
38 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/color/bottom_nav_item_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_person.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_apply.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_apps.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_bin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_glyphifier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
25 |
31 |
37 |
43 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notifications.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pause.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_ringtones_lib.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_share.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_single.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sleep.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sunlight.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/p1_glyph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/drawable/p1_glyph.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/p2_glyph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/drawable/p2_glyph.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/p2a_glyph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/drawable/p2a_glyph.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rounded_button_gray.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rounded_button_red.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rounded_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/style_expanded_toggle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/font/dot_matri.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/font/dot_matri.TTF
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
29 |
30 |
38 |
39 |
46 |
47 |
54 |
55 |
62 |
63 |
70 |
71 |
78 |
79 |
80 |
81 |
82 |
83 |
95 |
96 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_exact_alarm.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
26 |
27 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_notification_listener_disclaimer.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
26 |
27 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_output_name.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
29 |
30 |
38 |
39 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_perm_contacts.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
27 |
28 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_perm_notifications.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
27 |
28 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_perm_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
26 |
27 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_sleep_mode.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/first_boot.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
27 |
28 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_apps_choice.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_contacts_choice.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
34 |
35 |
50 |
51 |
64 |
65 |
66 |
67 |
73 |
74 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
21 |
22 |
30 |
31 |
43 |
44 |
56 |
57 |
69 |
70 |
71 |
72 |
73 |
84 |
85 |
93 |
94 |
105 |
106 |
118 |
119 |
131 |
132 |
144 |
145 |
157 |
158 |
159 |
160 |
161 |
162 |
176 |
177 |
195 |
196 |
206 |
207 |
215 |
216 |
223 |
224 |
225 |
226 |
227 |
241 |
242 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_notifications_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
29 |
30 |
35 |
36 |
47 |
48 |
60 |
61 |
73 |
74 |
86 |
87 |
88 |
89 |
103 |
104 |
105 |
106 |
107 |
108 |
119 |
120 |
127 |
128 |
145 |
146 |
163 |
164 |
165 |
182 |
183 |
199 |
200 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_notifications_2a.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
29 |
34 |
35 |
36 |
47 |
48 |
60 |
61 |
73 |
74 |
86 |
87 |
88 |
89 |
103 |
104 |
105 |
106 |
107 |
108 |
116 |
117 |
124 |
125 |
142 |
143 |
160 |
161 |
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_ringtones.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_ringtone.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_rr_centered_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fr4nKB/Glyphify/8ec2719f32cd5f919b6655c8033ad21d49855c43/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/navigation/mobile_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
15 |
16 |
17 |
22 |
25 |
28 |
29 |
30 |
35 |
36 |
41 |
44 |
45 |
46 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Glyphify
5 |
6 | Glyphifier
7 | Klingeltöne
8 | Benachrichtigungen
9 |
10 | OK
11 | Abbrechen
12 |
13 | Mache Glyphs wieder nützlich
14 | Glyphify Update
15 |
16 | Eine neue Version ist verfügbar, tippe zum Download bei GitHub
17 |
18 | Willkommen! Dies ist eine kostenlose und werbefreie App eines Studenten. Wenn du sie nützlich findest und deine Anerkennung zeigen möchtest, denke doch über eine kleine Spende nach oder vergib einen Stern für meine GitHub Repo. Jede Unterstützung ist willkommen. Danke fürs lesen!
19 | Gib einen Kaffee für mich aus
20 | Stern für meine Repo
21 |
22 |
23 |
24 | Datei wählen
25 | Spenden
26 |
27 | GIB EINEN NAMEN FÜR DEINEN KLINGELTON EIN
28 | Erlaube Benachrichtigungen wenn ein neues Update erscheint
29 |
30 | ANZAHL DER ZONEN
31 | 5 ZONEN
32 | 33 ZONEN
33 |
34 | Analysiere deinen Musiktitel…
35 | Erstelle einzigartige Animationen
36 | Fast geschafft…
37 |
38 | Bitte wähle eine Audiodatei mit maximal 5 Minuten Länge
39 | Bitte wähle eine Audiodatei
40 | Keine Datei
41 | Fehler beim laden der Datei
42 | Konvertierung zur WAV-Datei fehlgeschlagen
43 | Erstellung der Komposition fehlgeschlagen
44 | Fehler beim Export der Komposition zu einem Klingelton
45 | Es wurde kein Name für den Klingelton eingegeben
46 |
47 |
48 |
49 | Klingelton übernehmen
50 | Teilen
51 | Löschen
52 |
53 | Standard Klingelton übernommen
54 | Diese App muss Systemeinstellungen ändern, um den Klingelton zu übernehmen. Möchtest du dies erlauben?
55 |
56 |
57 |
58 | APP
59 | Startzeit
60 | Endzeit
61 |
62 | Erlaube den Zugriff auf Kontakte, um eine Benachrichtigung von diesem Kontakt zu erkennen
63 | In diesem Bereich kannst du jede Glyph Zone für eine "Essential" Benachrichtigung nutzen: Tippe auf eine Glyph Zone, wähle einen Kontakt und jedes Mal wenn du eine Benachrichtigung von diesem Kontakt erhältst, wird das Glyph aufleuchten bis du die Benachrichtigung weg wischst oder beantwortest. Um dies zu nutzen, musst du der App erlauben Benachrichtigungen und Deine Kontakte zu lesen.
64 | Erstelle einen Zeitplan, um die Extended Essential Benachrichtigungen zu einer bestimmten Zeit auszuschalten
65 |
66 | SCHLAFZEIT MODUS
67 |
68 | GLYPH INTENSITÄT
69 | NIEDRIG
70 | MITTEL
71 | HOCH
72 | VARIABEL
73 |
74 | Gedrückt halten, um einen Eintrag zu löschen
75 |
76 |
77 | - STATISCH
78 | - PULSIEREN
79 | - PING-PONG
80 | - !WAVE
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Glyphify
5 |
6 | Glyphifier
7 | Suonerie
8 | Notifiche
9 |
10 | OK
11 | Annulla
12 |
13 | Rendendo i Glpyhs di nuovo utili
14 | Aggiornamento Glyphify
15 |
16 | Una nuova versione è disponibile, premi per scaricare su GitHub
17 |
18 | Benvenuto! Questa è un\'app gratis e senza pubblicità fatta da uno studente. Se trovi valore nel mio lavoro e vorresti dimostrare il tuo apprezzamento, valuta la possibilità di fare una donazione o di assegnare una stella alla mia repository GitHub. Qualsiasi supporto è apprezzato, grazie per aver letto!
19 | Comprami un caffè
20 | Repository GitHub
21 |
22 |
23 |
24 | Seleziona un file
25 | Dona
26 |
27 | INSERISCI UN NOME PER LA SUONERIA
28 | Consenti all\'app di inviarti notifiche per ricevere aggiornamenti
29 |
30 | NUMERO ZONE
31 | 5 ZONE
32 | 33 ZONE
33 |
34 | Analizzando la tua canzone…
35 | Creando animazioni uniche
36 | Quasi fatto…
37 |
38 | Per favore seleziona un file audio di massimo 5 minuti
39 | Per favore selezione un file audio
40 | Questo file non esiste
41 | Caricamento del file selezionato fallito
42 | Conversione del file a WAV fallita
43 | Creazione della composizione fallita
44 | Esportazione composizione in suoneria fallita
45 | Nessun nome per la suoneria fornito
46 |
47 |
48 |
49 | Applica suoneria
50 | Condividi
51 | Elimina
52 |
53 | Suoneria di default applicata
54 | Questa app ha bisogno di cambiare le impostazioni di sistema per applicare la suoneria. Vuoi concedere il permesso?
55 |
56 |
57 |
58 | APP
59 | Start time
60 | End time
61 |
62 | Consenti di leggere i contatti per rilevare una notifica da un contatto
63 | Questo pannello ti consente di selezionare una qualsiasi zona Glyph come notifica "Essential": premi su una zona glyph, seleziona un contatto e ogni volta che ricevi una notifica da quel contatto la zona selezionata rimarrà accesa finchè non fai swipe o tap sulla notifica. Per utilizzare questa funzionalità devi garantire il permesso per leggere notifiche e contatti.\n\nTentando di consentire l\'accesso alle notifiche apparirà un messaggio di \'Impostazioni con limitazioni\', per consentire l\'accesso alle notifiche vai nell\'app Impostazioni -> App -> Glyphify -> tree punti, torna in Glyphify e consenti il permesso.\nI tre punti non saranno visibili se prima non è apparso l\'avviso di \'Impostazioni con limitazioni\'.
64 | Schedule Extended Essential to turn off at a set time
65 |
66 | SLEEP MODE
67 |
68 | INTENSITÀ GLYPH
69 | BASSA
70 | MEDIA
71 | ALTA
72 | VARIABILE
73 |
74 | Tieni premuto su un elemento per rimuoverlo
75 |
76 |
77 | - STATICO
78 | - RESPIRO
79 | - !PING-PONG
80 | - !ONDA
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Glyphify
5 |
6 | Glyphifier
7 | 着信音
8 | 通知
9 |
10 | OK
11 | 中止する
12 |
13 | Glyph の有用性を取り戻します
14 | Glyphify のアップデート
15 |
16 | 新しいバージョンが公開されています。タップして GitHub からダウンロードしてください。
17 |
18 | ようこそ! このアプリは、学生によって開発された広告のない無料なアプリです。あなたがもしも私のアプリに価値を感じたり、感謝をしたいときは寄付をしていただくか、私の GitHub リポジトリに Star を付けてください。どんなサポートも感謝します!
19 | コーヒーを買ってくれませんか
20 | リポジトリに Star を付ける
21 |
22 |
23 |
24 | ファイルを選択
25 | 寄付
26 |
27 | 着信音の名前を入力してください
28 | アップデートが公開されたときの通知を許可してください。
29 |
30 | ゾーンの数
31 | 5 個のゾーン
32 | 33 個のゾーン
33 |
34 | 曲を解析しています…
35 | ユニークなアニメーションを作成中
36 | あともう少しです…
37 |
38 | 最長 5 分までのオーディオファイルを選択してください
39 | オーディオファイルを選択してください
40 | ファイルはありません
41 | ファイルの読み込みに失敗しました
42 | WAV ファイルへの変換に失敗しました
43 | 着信音の作成に失敗しました
44 | 音源を着信音としてエクスポートできませんでした
45 | 着信音の名前が指定されていません
46 |
47 |
48 |
49 | 着信音を適用
50 | 共有
51 | 削除
52 |
53 | デフォルトの着信音を適用しました
54 | このアプリで着信音を適用するためにシステム設定を変更する必要があります。これを許可しますか?
55 |
56 |
57 |
58 | アプリ
59 | 開始する時間
60 | 終了する時間
61 |
62 | 連絡先からの通知を検出するために連絡先の読み取りを許可してください。
63 | このパネルは、任意の Glyph ゾーンを「Essential 通知」として選択できます。Glyph ゾーンをタップし、連絡先を選択するとその連絡先からの通知を受け取るたびにスワイプか、通知に応答するまで Glyph が点灯します。これを行なうには、アプリへの通知と連絡先のアクセスを許可する必要があります。
64 | 拡張された Essential 通知を停止するスケジュールを指定します。
65 |
66 | おやすみ時間モード
67 |
68 | GLYPH の強度
69 | 低
70 | 中
71 | 高
72 | バリアブル
73 |
74 | 要素を削除するには長押ししてください
75 |
76 |
77 | - 静的
78 | - パルス
79 | - !PING-PONG
80 | - !WAVE
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Glyphify
4 |
5 | Glyphifier
6 | Zil sesleri
7 | Bildirimler
8 |
9 | Tamam
10 | İptal et
11 |
12 | Glifleri tekrar kullanışlı hale getir
13 | Glyphify güncellemesi
14 |
15 | Yeni bir sürüm mevcut, GitHub\'dan indirmek için dokunun
16 |
17 | Hoş geldiniz! Bu bir öğrenci tarafından yapılmış, kullanımı ücretsiz ve reklamsız bir uygulamadır. Çalışmalarımızı değerli bulursanız ve takdir etmek isterseniz, bağış yapmayı veya GitHub depomuza bir yıldız vermeyi düşünün. Herhangi bir desteğiniz takdir edilir, bunu okuduğunuz için teşekkürler!
18 | Bana bir kahve ısmarla
19 | Depomu yıldızla
20 |
21 |
22 |
23 | Bir dosya seçin
24 | Bağış yapın
25 |
26 | ZİL SESİNİZ İÇİN BİR İSİM GİRİN
27 | Yeni bir güncelleme yayınlandığında bildirilmesine izin verin
28 |
29 | BÖLGE SAYISI
30 | 5 BÖLGE
31 | 33 BÖLGE
32 |
33 | Şarkınız analiz ediliyor…
34 | Benzersiz animasyonlar oluşturuluyor
35 | Neredeyse tamamlandı…
36 |
37 | Lütfen en fazla 5 dakikalık bir ses dosyası seçin
38 | Lütfen bir ses dosyası seçin
39 | Böyle bir dosya yok
40 | Dosya yüklenirken başarısız oldu
41 | WAV dosyasına dönüştürme başarısız oldu
42 | Beste oluşturma başarısız oldu
43 | Besteyi zil sesine aktarma başarısız oldu
44 | Zil sesi için herhangi bir isim sağlanmadı
45 |
46 |
47 |
48 | Zil sesini uygula
49 | Paylaş
50 | Sil
51 |
52 | Varsayılan zil sesi uygulandı
53 | Bu uygulamanın zil sesini uygulayabilmesi için sistem ayarlarını değiştirmesi gerekiyor. Buna izin vermek istiyor musunuz?
54 |
55 |
56 |
57 | Uygulama
58 | Başlangıç zamanı
59 | Bitiş zamanı
60 |
61 | Bir kişiden gelen bildirimi algılamak için kişileri okumaya izin verin
62 | Bu panel herhangi bir Glif bölgesini "Temel" bildirim olarak seçmenize olanak tanır: Bir Glif bölgesine dokunun, bir kişi seçin ve o kişiden her bildirim aldığınızda, bildirimi kaydırana veya yanıtlayana kadar Glif açık kalır. Bunu yapmak için uygulamanın bildirimleri ve kişilerinizi okumasına izin vermeniz gerekir.\n\nİzni vermeye çalıştığınızda, bir \'Kısıtlı ayarlar'\ iletişim kutusu açılacaktır. İzni vermek için Ayarlar uygulaması -> Uygulamalar -> Glyphify -> üç noktaya gidin, ardından izni vermek için Glyphify\'a geri dönün.\nÜç nokta, \'Kısıtlı ayarlar\' açılır penceresi gösterilene kadar üç nokta görünmeyecektir.
63 | Genişletilmiş Temel\'i belirli bir zamanda kapatmak için zamanlama yapın
64 |
65 | UYKU MODU
66 |
67 | GLİF YOĞUNLUĞU
68 | DÜŞÜK
69 | ORTA
70 | YÜKSEK
71 | DEĞİŞKEN
72 |
73 | Bir öğeyi kaldırmak için uzun dokunun
74 |
75 |
76 | - STATİK
77 | - NABIZ
78 | - !PİNG-PONG
79 | - !DALGA
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Glyphify
4 |
5 | Glyphifier
6 | 铃声
7 | 通知
8 |
9 | 确定
10 | 中止
11 |
12 | 让 Glyphs 更加有用
13 | Glyphify 更新
14 |
15 | 有新版本,前往 Github 更新。
16 |
17 | 欢迎!这是由一款由学生制作的免费使用且无广告的应用,如果您觉得我的作品有价值并且想表示支持,可以考虑捐款或给我的 Github 仓库点颗 Star,任何支持都表示感谢,谢谢阅读!
18 | 给我买杯咖啡
19 | 给仓库 Star
20 |
21 |
22 |
23 | 选择文件
24 | 捐款
25 |
26 | 为您的铃声命名
27 | 给予通知权限,以便当有新的更新时接收通知
28 |
29 | 区域数量
30 | 5 个区域
31 | 33 个区域
32 |
33 | 正在分析您的歌曲...
34 | 正在创建独特动画...
35 | 即将完成...
36 |
37 | 请选择不超过5分钟的音频文件
38 | 请选择有效的音频文件
39 | 文件不存在
40 | 文件加载失败
41 | WAV 格式转换失败
42 | 特效合成创建失败
43 | 铃声导出失败
44 | 未提供铃声名称
45 |
46 |
47 |
48 | 应用
49 | 分享
50 | 删除
51 |
52 | 默认铃声已应用
53 | 这款应用需要修改系统设置来应用铃声,是否允许?
54 |
55 |
56 |
57 | 应用
58 | 开始时间
59 | 结束时间
60 |
61 | 请允许读取联系人权限,以便识别来自特定联系人的通知
62 | 此面板允许您将任何 Glyph 区域设为"重要通知":点击 Glyph 区域并选择联系人,当收到该联系人的通知时,Glyph 灯效将保持常亮直到您查看或回复通知。\n\n需要授予通知读取和联系人读取权限。\n\n首次授权时会弹出"受限设置"提示,请前往:设置 → 应用 → Glyphify → 右上角 ⋮ 菜单 → 返回本应用完成授权( ⋮ 菜单需在弹出提示后才会显示)。
63 | 设置定时关闭扩展重要通知功能
64 |
65 | 睡眠模式
66 |
67 | 灯效强度
68 | 低
69 | 中
70 | 高
71 | 可变
72 |
73 | 长按可删除元素
74 |
75 |
76 | - 静态
77 | - 脉冲
78 | - !乒乓
79 | - !波浪
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #000000
9 | #FFFFFFFF
10 | #808080
11 | #D71A21
12 | #1C1D1F
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Glyphify
4 |
5 | Glyphifier
6 | Ringtones
7 | Notifications
8 |
9 | OK
10 | Abort
11 |
12 | Making Glyphs useful again
13 | Glyphify update
14 |
15 | A new version is available, tap to download on GitHub
16 |
17 | Welcome! This a free to use and ad-free app made by a student. If you find value in my work and would like to show your appreciation, consider making a donation or giving a star to my GitHub repo. Any support is appreciated, thanks for reading this!
18 | Buy me a coffee
19 | Star my repo
20 |
21 |
22 |
23 | Select a file
24 | Donate
25 |
26 | ENTER A NAME FOR YOUR RINGTONE
27 | Allow notifications to be notified when a new update is published
28 |
29 | NUMBER OF ZONES
30 | 5 ZONES
31 | 33 ZONES
32 |
33 | Analysing your song…
34 | Creating unique animations
35 | Almost there…
36 |
37 | Please select an audio file of maximum 5 minutes
38 | Please select an audio file
39 | No such file
40 | Failed at loading the file
41 | Conversion to a WAV file failed
42 | Creation of composition failed
43 | Failed at exporting the composition into a ringtone
44 | No name was provided for the ringtone
45 |
46 |
47 |
48 | Apply ringtone
49 | Share
50 | Delete
51 |
52 | Default ringtone applied
53 | This app needs to change system settings in order to apply the ringtone. Do you want to allow this?
54 |
55 |
56 |
57 | APP
58 | Start time
59 | End time
60 |
61 | Allow to read contacts in order to detect a notification from that contact
62 | This panel let\'s you chose any Glyph zone as a "Essential" notification: tap on a Glyph zone, select a contact and every time you get a notification from that contact the Glyph will stay on until you swipe or answer the notification. To do this you need to allow the app to read notifications and to read your contacts.\n\nBy attempting to allow the permission a \'Restricted settings\' dialog will popup, to allow the permission head into the Settings app -> Apps -> Glyphify -> three dots, then go back to Glyphify to allow the permission.\nThe three dots won\'t appear until the \'Restricted settings\' popup is shown.
63 | Schedule Extended Essential to turn off at a set time
64 |
65 | SLEEP MODE
66 |
67 | GLYPH INTENSITY
68 | LOW
69 | MID
70 | HIGH
71 | VARIABLE
72 |
73 | Long tap to remove an element
74 |
75 |
76 | - STATIC
77 | - PULSE
78 | - !PING-PONG
79 | - !WAVE
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/test/java/com/frank/glyphify/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.frank.glyphify
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------