├── 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.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](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.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](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.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](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