├── .gitignore ├── .idea ├── .gitignore ├── .name ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── playsho │ │ └── android │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fonts │ │ │ ├── roboto_bold.ttf │ │ │ ├── roboto_medium.ttf │ │ │ └── roboto_regular.ttf │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── playsho │ │ │ └── android │ │ │ ├── adapter │ │ │ └── MessageAdapter.kt │ │ │ ├── base │ │ │ ├── ApplicationLoader.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseAdapter.java │ │ │ ├── BaseBottomSheet.kt │ │ │ ├── BaseModel.kt │ │ │ └── BasePopup.kt │ │ │ ├── component │ │ │ ├── ActivityLauncher.kt │ │ │ ├── Button.kt │ │ │ ├── Icon.kt │ │ │ ├── PhotoBoard.kt │ │ │ └── Sheet.kt │ │ │ ├── config │ │ │ └── Conf.kt │ │ │ ├── data │ │ │ ├── Device.kt │ │ │ ├── Message.kt │ │ │ └── Room.kt │ │ │ ├── db │ │ │ └── SessionStorage.java │ │ │ ├── network │ │ │ ├── APIService.kt │ │ │ ├── Agent.kt │ │ │ ├── Errors.kt │ │ │ ├── Meta.kt │ │ │ ├── Response.kt │ │ │ ├── Result.kt │ │ │ ├── RetrofitClient.kt │ │ │ └── SocketManager.kt │ │ │ ├── ui │ │ │ ├── CinemaActivity.kt │ │ │ ├── RoomActivity.kt │ │ │ ├── SettingActivity.kt │ │ │ ├── SplashActivity.kt │ │ │ ├── bottomsheet │ │ │ │ ├── AddStreamLinkBottomSheet.kt │ │ │ │ ├── ChangeNameBottomSheet.kt │ │ │ │ ├── JoinRoomBottomSheet.kt │ │ │ │ └── SendMessageBottomSheet.kt │ │ │ └── popup │ │ │ │ └── CinemaSettingPopup.kt │ │ │ └── utils │ │ │ ├── AnimationHelper.kt │ │ │ ├── ClipboardHandler.kt │ │ │ ├── Crypto.kt │ │ │ ├── Debouncer.kt │ │ │ ├── DebugUtils.kt │ │ │ ├── DeviceUtils.kt │ │ │ ├── DimensionUtils.kt │ │ │ ├── KeyStoreHelper.kt │ │ │ ├── LocalController.kt │ │ │ ├── NetworkListener.kt │ │ │ ├── PlayerUtils.kt │ │ │ ├── RSAHelper.kt │ │ │ ├── SystemUtilities.kt │ │ │ ├── ThemeHelper.kt │ │ │ ├── Validator.kt │ │ │ └── accountmanager │ │ │ ├── AccountAuthenticator.kt │ │ │ ├── AccountAuthenticatorService.kt │ │ │ └── AccountInstance.kt │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_action_edit.png │ │ ├── ic_action_eye.png │ │ ├── ic_action_left_light.png │ │ ├── ic_action_logo_mini.png │ │ ├── ic_action_message.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_action_send_rocket.png │ │ ├── ic_action_setting.png │ │ ├── ic_action_smiley.png │ │ └── ic_action_youtube.png │ │ ├── drawable-mdpi │ │ ├── ic_action_edit.png │ │ ├── ic_action_eye.png │ │ ├── ic_action_left_light.png │ │ ├── ic_action_logo_mini.png │ │ ├── ic_action_message.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_action_send_rocket.png │ │ ├── ic_action_setting.png │ │ ├── ic_action_smiley.png │ │ └── ic_action_youtube.png │ │ ├── drawable-nodpi │ │ └── img_logo.png │ │ ├── drawable-xhdpi │ │ ├── ic_action_edit.png │ │ ├── ic_action_eye.png │ │ ├── ic_action_left_light.png │ │ ├── ic_action_logo_mini.png │ │ ├── ic_action_message.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_action_send_rocket.png │ │ ├── ic_action_setting.png │ │ ├── ic_action_smiley.png │ │ └── ic_action_youtube.png │ │ ├── drawable-xxhdpi │ │ ├── ic_action_edit.png │ │ ├── ic_action_eye.png │ │ ├── ic_action_left_light.png │ │ ├── ic_action_logo_mini.png │ │ ├── ic_action_message.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_action_send_rocket.png │ │ ├── ic_action_setting.png │ │ ├── ic_action_smiley.png │ │ └── ic_action_youtube.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_action_edit.png │ │ ├── ic_action_eye.png │ │ ├── ic_action_left_light.png │ │ ├── ic_action_logo_mini.png │ │ ├── ic_action_message.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_action_send_rocket.png │ │ ├── ic_action_setting.png │ │ ├── ic_action_smiley.png │ │ └── ic_action_youtube.png │ │ ├── drawable │ │ ├── bg_top_round.xml │ │ ├── chat_bubble_me.xml │ │ ├── chat_bubble_sender.xml │ │ ├── chat_bubble_system.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── font │ │ ├── roboto_bold.ttf │ │ ├── roboto_medium.ttf │ │ └── roboto_regular.ttf │ │ ├── layout │ │ ├── activity_cinema.xml │ │ ├── activity_room.xml │ │ ├── activity_setting.xml │ │ ├── activity_splash.xml │ │ ├── bottom_sheet_add_link.xml │ │ ├── bottom_sheet_change_name.xml │ │ ├── bottom_sheet_join_room.xml │ │ ├── bottom_sheet_send_message.xml │ │ ├── item_message_me.xml │ │ ├── item_message_sender.xml │ │ ├── item_message_system.xml │ │ └── popup_cinema_setting.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ ├── style.xml │ │ └── themes.xml │ │ └── xml │ │ ├── authenticator.xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── playsho │ └── android │ └── ExampleUnitTest.java ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── screenshot_four.png ├── screenshot_one.png ├── screenshot_three.png └── screenshot_two.png └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Playsho -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PlaySho Logo 3 |

4 | 5 | # PlaySho: Virtual Cinema for Shared Movie Nights 6 | 7 | Turn off those movie nights into immersive cinematic experiences with PlaySho! 🎬 You can watch the same video with your friends from anywhere. Create a virtual cinema, share a streaming link and enjoy synchronized playback to have unforgettable experience of watching a movie. 8 | 9 | 10 |

11 | Screenshot 1 12 | Screenshot 2 13 | Screenshot 3 14 | Screenshot 4 15 |

16 | 17 | ## 📱 Server Side 18 | For the Server Side of PlaySho, please visit the [PlaySho Nestjs repository](https://github.com/binaryb3ast/playsho-nest). 19 | 20 | ## 🚀 Key Features: 21 | 22 | ### 🎥 Virtual Cinema: 23 | 24 | Set up your own private cinema room and have a shared movie night with friends. 25 | 26 | ### 🍿 Single Video Playback: 27 | 28 | For that cinema-like feel, you can stream one video synchronously. 29 | 30 | ### 🔗 Stream Link Sharing: 31 | 32 | Create a special stream link and invite your friends to join in your virtual theatre. 33 | 34 | ### 💬 Real-Time Interaction: 35 | 36 | Chatting and sharing reactions among friends gives an opportunity to watch movies socially. 37 | 38 | ### 📱 Cross-Platform Accessibility: 39 | 40 | Use PlaySho on different gadgets, be it smartphone, tablet or desktop computer to have an adjustable and joyful movie night. 41 | 42 | ### 🌐 Stay Connected: 43 | 44 | Have common movie minutes with friends and family who are in different geographical locations. 45 | 46 | ## 🛠️ How It Works: 47 | 48 | 1. **Create Cinema Room:** 49 | - Set up a virtual cinema room that can be altered to suit the right mood for a perfect movie night. 50 | 51 | 2. **Choose Your Movie:** 52 | - Choose one video that will be enjoyed by all people together. 53 | 54 | 3. **Generate Stream Link:** 55 | - Share a unique streaming link with your buddies. 56 | 57 | 4. **Watch Together:** 58 | - Experience the magic of synchronized playback as you watch a single video together in real-time. 59 | 60 | ## 🌟 Why PlaySho: 61 | 62 | - **Virtual Movie Nights:** 63 | - Make shared film nights feel like you are actually at the theatre or cinema hall 64 | 65 | - **Synchronized Playback:** 66 | - Have perfect timing and synchronization that is characteristic of watching movies from cinemas 67 | 68 | - **No Limits:** 69 | - Friends might be watching from anywhere making distances irrelevant 70 | 71 | ## 🎉 Turn your virtual cinema dreams into reality with PlaySho. Download now and start hosting unforgettable shared movie nights! 🍿✨ 72 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.playsho.android" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "com.playsho.android" 12 | minSdk = 24 13 | targetSdk = 34 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | 21 | 22 | kotlinOptions { 23 | jvmTarget = "1.8" 24 | } 25 | 26 | 27 | buildFeatures { 28 | dataBinding = true 29 | viewBinding = true 30 | 31 | } 32 | 33 | buildTypes { 34 | release { 35 | isMinifyEnabled = false 36 | proguardFiles( 37 | getDefaultProguardFile("proguard-android-optimize.txt"), 38 | "proguard-rules.pro" 39 | ) 40 | } 41 | } 42 | packaging { 43 | resources { 44 | excludes +="META-INF/rxjava.properties" 45 | } 46 | } 47 | compileOptions { 48 | sourceCompatibility = JavaVersion.VERSION_1_8 49 | targetCompatibility = JavaVersion.VERSION_1_8 50 | } 51 | } 52 | 53 | dependencies { 54 | 55 | implementation(libs.androidx.appcompat) 56 | implementation(libs.material) 57 | implementation(libs.androidx.constraintlayout) 58 | implementation(libs.androidx.core.ktx) 59 | testImplementation(libs.junit) 60 | androidTestImplementation(libs.androidx.junit) 61 | androidTestImplementation(libs.androidx.espresso.core) 62 | implementation("androidx.core:core-splashscreen:1.1.0-rc01") 63 | 64 | implementation("com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0") 65 | implementation("com.squareup.retrofit2:adapter-rxjava:2.9.0") 66 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 67 | implementation("com.squareup.retrofit2:converter-scalars:2.9.0") 68 | implementation("com.squareup.retrofit2:converter-gson:2.9.0") 69 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.12") 70 | implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12") 71 | 72 | implementation("androidx.datastore:datastore-preferences:1.0.0") 73 | 74 | implementation("androidx.media3:media3-exoplayer:1.2.1") 75 | implementation("androidx.media3:media3-ui:1.2.1") 76 | implementation("androidx.media3:media3-exoplayer-dash:1.2.1") 77 | 78 | implementation("com.squareup.picasso:picasso:2.71828") 79 | 80 | implementation("com.github.razir.progressbutton:progressbutton:2.1.0") 81 | 82 | implementation("androidx.annotation:annotation-jvm:1.7.1") 83 | 84 | implementation("io.socket:socket.io-client:2.0.0") { 85 | exclude(group = "org.json", module = "json") 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/playsho/android/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.playsho.android; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.playsho.android", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 22 | 23 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/assets/fonts/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/assets/fonts/roboto_bold.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/assets/fonts/roboto_medium.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/assets/fonts/roboto_regular.ttf -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/adapter/MessageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.adapter 2 | 3 | import android.graphics.Color 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.LayoutInflater 7 | import android.view.ViewGroup 8 | import android.view.animation.AlphaAnimation 9 | import android.view.animation.Animation 10 | import android.view.animation.Animation.AnimationListener 11 | import android.view.animation.TranslateAnimation 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.playsho.android.data.Message 14 | import com.playsho.android.databinding.ItemMessageMeBinding 15 | import com.playsho.android.databinding.ItemMessageSenderBinding 16 | import com.playsho.android.databinding.ItemMessageSystemBinding 17 | import com.playsho.android.utils.AnimationHelper 18 | import com.playsho.android.utils.accountmanager.AccountInstance 19 | 20 | 21 | class MessageAdapter(private val dataSet: MutableList) : 22 | RecyclerView.Adapter() { 23 | 24 | companion object { 25 | const val SENDER = 2 26 | const val SYSTEM = 0 27 | const val ME = 1 28 | } 29 | 30 | override fun getItemCount(): Int { 31 | return dataSet.size 32 | } 33 | 34 | override fun getItemViewType(position: Int): Int { 35 | return if (dataSet[position].type == "system") { 36 | SYSTEM 37 | } else if (dataSet[position].sender.tag == AccountInstance.getUserData("tag")) {//me 38 | SENDER 39 | } else { 40 | SENDER 41 | } 42 | } 43 | 44 | // Create new views (invoked by the layout manager) 45 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 46 | return when (viewType) { 47 | SENDER -> { 48 | val inflater = LayoutInflater.from(parent.context) 49 | val binding = ItemMessageSenderBinding.inflate(inflater, parent, false) 50 | SenderViewHolder(binding) 51 | } 52 | 53 | ME -> { 54 | val inflater = LayoutInflater.from(parent.context) 55 | val binding = ItemMessageMeBinding.inflate(inflater, parent, false) 56 | MyselfViewHolder(binding) 57 | } 58 | 59 | else -> { 60 | val inflater = LayoutInflater.from(parent.context) 61 | val binding = ItemMessageSystemBinding.inflate(inflater, parent, false) 62 | SystemViewHolder(binding) 63 | } 64 | } 65 | } 66 | 67 | fun addMessage(message: Message) { 68 | dataSet.add(0, message) 69 | notifyItemInserted(0) 70 | } 71 | 72 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 73 | val message = dataSet[position] 74 | AnimationHelper.slideInFromBottom(holder.itemView , AnimationHelper.Duration.SLIDE_IN_FROM_BOTTOM) 75 | when (holder) { 76 | is SenderViewHolder -> holder.bind(message) 77 | is MyselfViewHolder -> holder.bind(message) 78 | is SystemViewHolder -> holder.bind(message) 79 | } 80 | Handler(Looper.getMainLooper()).postDelayed({ 81 | dataSet.remove(message) 82 | notifyItemRemoved(holder.bindingAdapterPosition) 83 | }, AnimationHelper.Duration.DISAPPEAR_MESSAGE) // 5 seconds delay 84 | } 85 | 86 | 87 | inner class SenderViewHolder(private val binding: ItemMessageSenderBinding ) : 88 | RecyclerView.ViewHolder(binding.root) { 89 | fun bind(message: Message) { 90 | binding.apply { 91 | message.also { 92 | txtMessage.text = message.message 93 | txtName.text = message.sender.userName 94 | txtName.setTextColor(Color.parseColor(message.sender.color)) 95 | } 96 | 97 | } 98 | } 99 | } 100 | 101 | inner class SystemViewHolder(private val binding: ItemMessageSystemBinding) : 102 | RecyclerView.ViewHolder(binding.root) { 103 | fun bind(message: Message) { 104 | binding.apply { 105 | message.also { 106 | txtMessage.text = message.message 107 | } 108 | 109 | } 110 | } 111 | } 112 | 113 | inner class MyselfViewHolder(private val binding: ItemMessageMeBinding) : 114 | RecyclerView.ViewHolder(binding.root) { 115 | fun bind(message: Message) { 116 | binding.apply { 117 | message.also { 118 | txtMessage.text = message.message 119 | } 120 | } 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/base/ApplicationLoader.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.base 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import androidx.appcompat.app.AppCompatDelegate 7 | import com.google.gson.Gson 8 | import com.playsho.android.db.SessionStorage 9 | import com.playsho.android.utils.DimensionUtils 10 | 11 | /** 12 | * Custom Application class that initializes the application context, Gson, and SessionStorage. 13 | */ 14 | class ApplicationLoader : Application() { 15 | 16 | companion object { 17 | lateinit var context: Context 18 | private lateinit var gson: Gson 19 | private lateinit var sessionStorage: SessionStorage 20 | 21 | 22 | /** 23 | * Gets the SessionStorage instance for managing session storage. 24 | * 25 | * @return The SessionStorage instance. 26 | */ 27 | @JvmStatic 28 | fun getSessionStorage(): SessionStorage { 29 | return sessionStorage 30 | } 31 | } 32 | 33 | /** 34 | * Called when the application is first created. Initializes the application context, Gson, and SessionStorage. 35 | */ 36 | override fun onCreate() { 37 | super.onCreate() 38 | context = applicationContext 39 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 40 | gson = Gson() 41 | sessionStorage = SessionStorage() 42 | try { 43 | DimensionUtils.checkDisplaySize(context, null) 44 | } catch (e: Exception) { 45 | e.printStackTrace() 46 | } 47 | } 48 | 49 | /** 50 | * Called when the configuration of the device changes. 51 | * 52 | * @param newConfig The new configuration. 53 | */ 54 | override fun onConfigurationChanged(newConfig: Configuration) { 55 | super.onConfigurationChanged(newConfig) 56 | try { 57 | DimensionUtils.checkDisplaySize(context, newConfig) 58 | } catch (e: Exception) { 59 | e.printStackTrace() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.base 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.ConnectivityManager 7 | import android.os.Bundle 8 | import androidx.activity.OnBackPressedCallback 9 | import androidx.activity.result.ActivityResult 10 | import androidx.annotation.ColorRes 11 | import androidx.annotation.LayoutRes 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.appcompat.app.AppCompatDelegate 14 | import androidx.databinding.DataBindingUtil 15 | import androidx.databinding.ViewDataBinding 16 | import com.playsho.android.component.ActivityLauncher 17 | import com.playsho.android.db.SessionStorage 18 | import com.playsho.android.utils.NetworkListener 19 | import com.playsho.android.utils.SystemUtilities 20 | import java.util.concurrent.atomic.AtomicBoolean 21 | 22 | /** 23 | * A base activity class that provides common functionality and structure for other activities. 24 | * 25 | * @param B The type of ViewDataBinding used by the activity. 26 | */ 27 | abstract class BaseActivity : AppCompatActivity() { 28 | 29 | // Flag to enable or disable the custom back press callback 30 | private var isBackPressCallbackEnable = false 31 | 32 | // View binding instance 33 | protected lateinit var binding: B 34 | protected lateinit var networkListener: NetworkListener 35 | // Activity launcher instance 36 | protected val activityLauncher = ActivityLauncher.registerActivityForResult(this) 37 | 38 | // Tag for logging 39 | protected open val TAG: String = javaClass.simpleName 40 | 41 | /** 42 | * Abstract method to get the layout resource ID for the activity. 43 | * 44 | * @return The layout resource ID. 45 | */ 46 | protected abstract fun getLayoutResourceId(): Int 47 | 48 | /** 49 | * Abstract method to handle custom behavior on back press. 50 | */ 51 | protected abstract fun onBackPress() 52 | 53 | /** 54 | * Gets a String extra from the Intent. 55 | * 56 | * @param key The key of the extra. 57 | * @return The String extra value. 58 | */ 59 | protected fun getIntentStringExtra(key: String): String? { 60 | return intent.getStringExtra(key) 61 | } 62 | 63 | /** 64 | * Gets the name of the current activity. 65 | * 66 | * @return The name of the activity. 67 | */ 68 | protected fun getClassName(): String { 69 | return javaClass.simpleName 70 | } 71 | 72 | override fun onCreate(savedInstanceState: Bundle?) { 73 | super.onCreate(savedInstanceState) 74 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 75 | binding = DataBindingUtil.setContentView(this, getLayoutResourceId()) 76 | networkListener = NetworkListener() 77 | onBackPressedDispatcher.addCallback(this, onBackPressedCallback) 78 | } 79 | 80 | override fun onDestroy() { 81 | super.onDestroy() 82 | onBackPressedCallback.remove() 83 | } 84 | 85 | private val onBackPressedCallback = object : OnBackPressedCallback(isBackPressCallbackEnable) { 86 | override fun handleOnBackPressed() { 87 | // Call the abstract method for custom back press handling 88 | onBackPress() 89 | } 90 | } 91 | 92 | /** 93 | * Sets whether the custom back press callback is enabled or disabled. 94 | * 95 | * @param isBackPressCallbackEnable true to enable the callback, false to disable it. 96 | */ 97 | fun setBackPressedCallBackEnable(isBackPressCallbackEnable: Boolean) { 98 | this.isBackPressCallbackEnable = isBackPressCallbackEnable 99 | } 100 | 101 | /** 102 | * Sets the status bar color using the SystemUtilities class. 103 | * 104 | * @param color The color resource ID. 105 | * @param isDark true if the status bar icons should be dark, false otherwise. 106 | */ 107 | protected fun setStatusBarColor(@ColorRes color: Int, isDark: Boolean) { 108 | SystemUtilities.changeStatusBarBackgroundColor(activity(), color, isDark) 109 | } 110 | 111 | /** 112 | * Gets the context of the activity. 113 | * 114 | * @return The context of the activity. 115 | */ 116 | protected fun context(): Context { 117 | return this 118 | } 119 | 120 | /** 121 | * Gets the activity instance. 122 | * 123 | * @return The activity instance. 124 | */ 125 | protected fun activity(): Activity { 126 | return this 127 | } 128 | 129 | /** 130 | * Gets the SessionStorage instance from the ApplicationLoader. 131 | * 132 | * @return The SessionStorage instance. 133 | */ 134 | protected fun getSessionManager(): SessionStorage { 135 | return ApplicationLoader.getSessionStorage() 136 | } 137 | 138 | /** 139 | * Checks if the network is available using the NetworkListener. 140 | * 141 | * @return true if the network is available, false otherwise. 142 | */ 143 | protected fun isNetworkAvailable(): AtomicBoolean { 144 | return networkListener.isNetworkAvailable 145 | } 146 | 147 | override fun onResume() { 148 | super.onResume() 149 | val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 150 | connectivityManager.registerDefaultNetworkCallback(networkListener) 151 | } 152 | 153 | override fun onPause() { 154 | super.onPause() 155 | val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 156 | connectivityManager.unregisterNetworkCallback(networkListener) 157 | } 158 | 159 | protected inline fun Activity.openActivity(vararg params: Pair) { 160 | val intent = createIntent().apply { 161 | for (param in params) { 162 | val (key, value) = param 163 | when (value) { 164 | is String -> putExtra(key, value) 165 | is Int -> putExtra(key, value) 166 | // Add more cases for other data types as needed 167 | } 168 | } 169 | } 170 | startActivity(intent) 171 | } 172 | 173 | protected inline fun Context.createIntent() = 174 | Intent(this, T::class.java) 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/base/BaseBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.base 2 | 3 | import android.content.DialogInterface 4 | import android.os.Bundle 5 | import android.support.annotation.Nullable 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.coordinatorlayout.widget.CoordinatorLayout 10 | import androidx.databinding.DataBindingUtil 11 | import androidx.databinding.ViewDataBinding 12 | import com.google.android.material.bottomsheet.BottomSheetBehavior 13 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 14 | import com.playsho.android.R 15 | import com.playsho.android.db.SessionStorage 16 | 17 | abstract class BaseBottomSheet : BottomSheetDialogFragment() { 18 | 19 | protected lateinit var binding: B 20 | private var origin: String? = null 21 | protected var bottomSheetStatusCallback: BottomSheetStatusCallback? = null 22 | protected var bottomSheetResultCallback: BottomSheetResultCallback? = null 23 | private lateinit var bottomSheetBehavior: BottomSheetBehavior<*> 24 | 25 | interface BottomSheetStatusCallback { 26 | fun onBottomSheetShow() 27 | fun onBottomSheetDismiss() 28 | } 29 | 30 | interface BottomSheetResultCallback { 31 | fun onBottomSheetProcessSuccess(data: String) 32 | fun onBottomSheetProcessFail(data: String) 33 | } 34 | 35 | protected abstract fun getLayoutResourceId(): Int 36 | protected abstract fun initView() 37 | 38 | init { 39 | val parentFragment = parentFragment 40 | setOrigin(parentFragment?.javaClass?.simpleName ?: activity?.javaClass?.simpleName ?: "") 41 | } 42 | 43 | fun setStatusCallback(bottomSheetCallback: BottomSheetStatusCallback) { 44 | this.bottomSheetStatusCallback = bottomSheetCallback 45 | } 46 | 47 | fun setResultCallback(callback: BottomSheetResultCallback) { 48 | this.bottomSheetResultCallback = callback 49 | } 50 | 51 | protected fun setOrigin(origin: String) { 52 | this.origin = origin 53 | } 54 | 55 | protected fun getOrigin(): String? { 56 | return this.origin 57 | } 58 | 59 | @Nullable 60 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 61 | binding = DataBindingUtil.inflate(inflater, getLayoutResourceId(), container, false) 62 | return binding.root 63 | } 64 | 65 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 66 | super.onViewCreated(view, savedInstanceState) 67 | 68 | } 69 | 70 | protected fun getSessionStorage(): SessionStorage { 71 | return ApplicationLoader.getSessionStorage() 72 | } 73 | 74 | fun show() { 75 | show(parentFragmentManager, getOrigin()) 76 | } 77 | 78 | override fun getTheme(): Int { 79 | return R.style.AppBottomSheetDialogTheme 80 | } 81 | 82 | override fun onDismiss(dialog: DialogInterface) { 83 | super.onDismiss(dialog) 84 | bottomSheetStatusCallback?.onBottomSheetDismiss() 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/base/BaseModel.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.base 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import org.jetbrains.annotations.NotNull 5 | import java.io.Serializable 6 | import java.util.UUID 7 | 8 | /** 9 | * Base model class for common attributes shared among other model classes. 10 | */ 11 | class BaseModel : Serializable { 12 | var hash: String = UUID.randomUUID().toString() 13 | 14 | @SerializedName("code") 15 | var code: String? = null 16 | 17 | @SerializedName("_id") 18 | var id: String? = null 19 | 20 | @SerializedName("created_at") 21 | var createdAt: String? = null 22 | 23 | @SerializedName("updated_at") 24 | var updatedAt: String? = null 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/base/BasePopup.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.base 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.ColorDrawable 5 | import android.transition.Transition 6 | import android.util.AttributeSet 7 | import android.view.LayoutInflater 8 | import android.view.MotionEvent 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.PopupWindow 12 | import androidx.annotation.Nullable 13 | import androidx.databinding.DataBindingUtil 14 | import androidx.databinding.ViewDataBinding 15 | 16 | /** 17 | * BasePopup is an abstract class that provides a foundation for creating custom popup windows in Android applications. 18 | * It extends the PopupWindow class and includes common functionalities for handling popups. 19 | * To use this class, you need to extend it and implement the required methods. 20 | * Usage Example: 21 | * ```kotlin 22 | * class CustomPopup : BasePopup() { 23 | * 24 | * override fun getLayoutResourceId(): Int { 25 | * // Return the layout resource ID for your custom popup layout 26 | * return R.layout.your_popup_layout 27 | * } 28 | * 29 | * override fun onViewInitialized() { 30 | * // Initialize your views and set any necessary listeners 31 | * // This method is called after inflating the view and before showing the popup 32 | * } 33 | * } 34 | * ``` 35 | * Features: 36 | * - Supports data binding with ViewDataBinding. 37 | * - Provides methods for showing the popup at a specified location or at the center of an anchor view. 38 | * - Allows customization of enter transition. 39 | * 40 | * @param T The type of ViewDataBinding used in the popup. 41 | */ 42 | abstract class BasePopup(context: Context) : PopupWindow(context) { 43 | 44 | /** 45 | * The ViewDataBinding instance associated with the popup layout. 46 | */ 47 | protected lateinit var binding: T 48 | 49 | /** 50 | * Inflates the popup view and performs necessary setup. 51 | */ 52 | private fun inflateView(context: Context) { 53 | val inflater = LayoutInflater.from(context) 54 | binding = DataBindingUtil.inflate(inflater, getLayoutResourceId(), null, false) 55 | setContentView(binding.root) 56 | setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) 57 | 58 | width = ViewGroup.LayoutParams.WRAP_CONTENT 59 | height = ViewGroup.LayoutParams.WRAP_CONTENT 60 | isOutsideTouchable = true 61 | 62 | // Custom initialization for the view 63 | onViewInitialized() 64 | } 65 | 66 | 67 | /** 68 | * Abstract method to get the layout resource ID for the popup. 69 | * 70 | * @return The layout resource ID. 71 | */ 72 | protected abstract fun getLayoutResourceId(): Int 73 | 74 | /** 75 | * Abstract method called after inflating the view and before showing the popup. 76 | * Use this method to initialize views and set any necessary listeners. 77 | */ 78 | protected abstract fun onViewInitialized() 79 | 80 | /** 81 | * Shows the popup at the specified location, triggered by a MotionEvent. 82 | * 83 | * @param anchor The anchor view. 84 | * @param event The motion event that triggered the popup. 85 | */ 86 | fun showAtLocation(anchor: View, event: MotionEvent) { 87 | // Implementation for showing the popup at a specific location 88 | } 89 | 90 | /** 91 | * Shows the popup at the center of the anchor view. 92 | * 93 | * @param anchor The anchor view. 94 | */ 95 | fun showAtLocation(anchor: View) { 96 | val location = IntArray(2) 97 | anchor.getLocationOnScreen(location) 98 | val x = location[0] + anchor.width / 2 99 | val y = location[1] + anchor.height / 2 100 | showAtLocation(anchor, 0, x, y) 101 | } 102 | 103 | init { 104 | inflateView(context) 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/component/ActivityLauncher.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.component 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.ActivityResult 5 | import androidx.activity.result.ActivityResultCaller 6 | import androidx.activity.result.ActivityResultLauncher 7 | import androidx.activity.result.contract.ActivityResultContract 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.annotation.NonNull 10 | import androidx.annotation.Nullable 11 | 12 | class ActivityLauncher private constructor( 13 | private val launcher: ActivityResultLauncher, 14 | private var onActivityResult: OnActivityResult? 15 | ) { 16 | interface OnActivityResult { 17 | fun onActivityResult(result: O) 18 | } 19 | 20 | companion object { 21 | @NonNull 22 | @JvmStatic 23 | fun registerForActivityResult( 24 | caller: ActivityResultCaller, 25 | contract: ActivityResultContract, 26 | onActivityResult: OnActivityResult? 27 | ): ActivityLauncher { 28 | return ActivityLauncher(caller.registerForActivityResult(contract) { 29 | onActivityResult?.onActivityResult(it) 30 | }, onActivityResult) 31 | } 32 | 33 | @NonNull 34 | @JvmStatic 35 | fun registerForActivityResult( 36 | caller: ActivityResultCaller, 37 | contract: ActivityResultContract 38 | ): ActivityLauncher { 39 | return registerForActivityResult(caller, contract, null) 40 | } 41 | 42 | @NonNull 43 | @JvmStatic 44 | fun registerActivityForResult(caller: ActivityResultCaller): ActivityLauncher { 45 | return registerForActivityResult( 46 | caller, 47 | ActivityResultContracts.StartActivityForResult() 48 | ) 49 | } 50 | } 51 | 52 | fun setOnActivityResult(onActivityResult: OnActivityResult?) { 53 | this.onActivityResult = onActivityResult 54 | } 55 | 56 | fun launch(input: Input, onActivityResult: OnActivityResult?) { 57 | this.onActivityResult = onActivityResult 58 | launcher.launch(input) 59 | } 60 | 61 | fun launch(input: Input) { 62 | launch(input, onActivityResult) 63 | } 64 | 65 | private fun callOnActivityResult(result: Result) { 66 | onActivityResult?.onActivityResult(result) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/component/Button.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.component 2 | 3 | import android.content.Context 4 | import android.content.res.TypedArray 5 | import android.graphics.Color 6 | import android.graphics.drawable.GradientDrawable 7 | import android.os.Build 8 | import android.util.AttributeSet 9 | import android.view.Gravity 10 | import androidx.appcompat.widget.AppCompatButton 11 | import androidx.appcompat.widget.AppCompatImageView 12 | import androidx.core.widget.ImageViewCompat 13 | import com.playsho.android.R 14 | import com.github.razir.progressbutton.DrawableButton 15 | import com.github.razir.progressbutton.attachTextChangeAnimator 16 | import com.github.razir.progressbutton.hideProgress 17 | import com.github.razir.progressbutton.showProgress 18 | import com.playsho.android.utils.LocalController 19 | import com.playsho.android.utils.ThemeHelper 20 | 21 | class Button @JvmOverloads constructor( 22 | context: Context, 23 | attrs: AttributeSet? = null, 24 | defStyleAttr: Int = 0 25 | ) : AppCompatButton(context, attrs, defStyleAttr) { 26 | 27 | private var mode: ButtonMode = ButtonMode.PRIMARY; 28 | private var isClickDisable: Boolean = false; 29 | private var text: String = "" 30 | 31 | enum class ButtonMode { 32 | PRIMARY, 33 | SECONDARY 34 | } 35 | 36 | init { 37 | if (!isInEditMode) { 38 | val a: TypedArray = 39 | context.obtainStyledAttributes(attrs, R.styleable.Button, defStyleAttr, 0) 40 | mode = when (a.getString(R.styleable.Button_mode) ?: "p") { 41 | "p" -> ButtonMode.PRIMARY 42 | "s" -> ButtonMode.SECONDARY 43 | else -> ButtonMode.PRIMARY // Default value 44 | } 45 | isClickDisable = a.getBoolean(R.styleable.Button_mode, false) 46 | a.recycle() 47 | } 48 | text = getText().toString() 49 | attachTextChangeAnimator { 50 | fadeInMills = 300 51 | fadeOutMills = 300 52 | } 53 | setBackgroundDrawable( 54 | if (isClickDisable) 55 | this.getDisableDrawable() 56 | else this.getPrimaryDrawable() 57 | ) 58 | setTextColor( 59 | LocalController.getColor( 60 | if (isClickDisable) R.color.button_text_deactive 61 | else R.color.button_text_active 62 | ) 63 | ) 64 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 65 | typeface = LocalController.getFont(R.font.roboto_bold) 66 | }else{ 67 | typeface = LocalController.getFont("roboto_bold.ttf") 68 | } 69 | textSize = 14f 70 | gravity = Gravity.CENTER 71 | } 72 | 73 | private fun getPrimaryDrawable(): GradientDrawable { 74 | return ThemeHelper.createRect(R.color.button_primary, 28) 75 | } 76 | 77 | private fun getDisableDrawable(): GradientDrawable { 78 | return ThemeHelper.createRect(R.color.button_disable, 28) 79 | } 80 | 81 | fun startProgress() { 82 | this.showProgress { 83 | progressColor = Color.WHITE 84 | gravity = DrawableButton.GRAVITY_CENTER 85 | } 86 | isEnabled = false 87 | 88 | } 89 | 90 | fun stopProgress() { 91 | hideProgress(text) 92 | isEnabled = true 93 | } 94 | 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/component/Icon.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.component 2 | 3 | import android.content.Context 4 | import android.content.res.TypedArray 5 | import android.graphics.PorterDuff 6 | import android.net.Uri 7 | import android.util.AttributeSet 8 | import android.widget.TextView 9 | import androidx.appcompat.widget.AppCompatButton 10 | import androidx.appcompat.widget.AppCompatImageView 11 | import androidx.appcompat.widget.AppCompatTextView 12 | import androidx.lifecycle.LifecycleOwner 13 | import androidx.lifecycle.lifecycleScope 14 | import com.playsho.android.R 15 | import com.playsho.android.utils.LocalController 16 | import com.squareup.picasso.Picasso 17 | import kotlinx.coroutines.delay 18 | import kotlinx.coroutines.launch 19 | import java.io.File 20 | 21 | class Icon @JvmOverloads constructor( 22 | context: Context, 23 | attrs: AttributeSet? = null, 24 | defStyleAttr: Int = 0 25 | ) : AppCompatImageView (context, attrs, defStyleAttr) { 26 | 27 | 28 | init { 29 | if (!isInEditMode) { 30 | attrs?.let { 31 | val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.Icon, defStyleAttr, 0) 32 | val color = a.getColor(R.styleable.Icon_color, 0) 33 | if (color != 0) { 34 | setColorFilter(color, PorterDuff.Mode.SRC_ATOP) 35 | } 36 | invalidate() 37 | a.recycle() 38 | } 39 | 40 | } 41 | } 42 | 43 | fun setColor(color: Int) { 44 | if (color != 0) { 45 | setColorFilter(LocalController.getColor(color), PorterDuff.Mode.SRC_ATOP) 46 | } 47 | invalidate() 48 | } 49 | 50 | fun load(uri: Uri) { 51 | Picasso.get().load(uri).into(this) 52 | } 53 | 54 | fun load(file: File) { 55 | Picasso.get().load(file).into(this) 56 | } 57 | 58 | fun load(path: String) { 59 | Picasso.get().load(path).into(this) 60 | } 61 | 62 | fun load(resourceId: Int) { 63 | Picasso.get().load(resourceId).into(this) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/component/PhotoBoard.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.component 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatImageView 6 | import androidx.core.widget.ImageViewCompat 7 | 8 | class PhotoBoard : AppCompatImageView { 9 | constructor(context: Context) : super(context) { 10 | // Initialize your custom view 11 | } 12 | 13 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 14 | // Initialize your custom view with attributes 15 | } 16 | 17 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 18 | // Initialize your custom view with attributes and style 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/component/Sheet.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.component 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.TextView 6 | import androidx.appcompat.widget.AppCompatButton 7 | import androidx.appcompat.widget.AppCompatTextView 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.lifecycleScope 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.launch 12 | 13 | class Sheet @JvmOverloads constructor( 14 | context: Context, 15 | attrs: AttributeSet? = null, 16 | defStyleAttr: Int = 0 17 | ) : AppCompatTextView(context, attrs, defStyleAttr) { 18 | 19 | init { 20 | 21 | } 22 | 23 | 24 | 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/config/Conf.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.config 2 | 3 | object Conf { 4 | 5 | object HttpHeader { 6 | const val AUTHORIZATION = "Authorization" 7 | const val CONTENT_TYPE = "Content-Type" 8 | } 9 | 10 | object Path { 11 | const val V1_DEVICE_GENERATE = "/v1/api/device/generate" 12 | const val V1_DEVICE_NAME = "/v1/api/device/name" 13 | const val V1_DEVICE_KEYPAIR_REGENERATE = "/v1/api/device/keypair/regenerate" 14 | const val V1_ROOM_CREATE = "/v1/api/room" 15 | const val V1_ROOM_GET = "/v1/api/room/{tag}" 16 | const val V1_ROOM_ENTRANCE = "/v1/api/room/{tag}/entrance" 17 | const val V1_ROOM_LINK = "/v1/api/room/{tag}/link" 18 | } 19 | 20 | object Query { 21 | const val STREAM_LINK = "stream_link" 22 | const val OS = "os" 23 | const val APP_VER = "app_version" 24 | const val APP_VER_NAME = "app_version_name" 25 | const val OS_VER = "os_version" 26 | const val LAST_UPDATE_TIME = "last_update_at" 27 | const val FIRST_INSTALL_TIME = "first_install_at" 28 | const val MANUFACTURER = "manufacturer" 29 | const val MODEL = "device_model" 30 | const val BRAND = "brand" 31 | const val FCM = "fcm" 32 | const val SECRET = "secret" 33 | const val PUBLIC_KEY = "public_key" 34 | const val USER_NAME = "user_name" 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/data/Device.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.data 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Device( 7 | 8 | @SerializedName("tag") 9 | @Expose 10 | val tag: String? = "", 11 | 12 | @SerializedName("name") 13 | @Expose 14 | val name: String? = "", 15 | 16 | @SerializedName("color") 17 | @Expose 18 | var color: String? = "#f46c7d", 19 | 20 | @SerializedName("brand") 21 | @Expose 22 | val brand: String, 23 | 24 | @SerializedName("model") 25 | @Expose 26 | val model: String, 27 | 28 | @SerializedName("os") 29 | @Expose 30 | val os: String, 31 | 32 | @SerializedName("secret") 33 | @Expose 34 | val secret: String? = "", 35 | 36 | @SerializedName("manufacturer") 37 | @Expose 38 | val manufacturer: String, 39 | 40 | @SerializedName("os_version") 41 | @Expose 42 | val osVersion: String, 43 | 44 | @SerializedName("token") 45 | @Expose 46 | val token: String? = "", 47 | 48 | @SerializedName("user_name") 49 | @Expose 50 | val userName: String? = "unknown" , 51 | 52 | @SerializedName("app_version_name") 53 | @Expose 54 | val appVersionName: String, 55 | 56 | @SerializedName("app_version") 57 | @Expose 58 | val appVersion: Int, 59 | 60 | @SerializedName("last_update_at") 61 | @Expose 62 | val lastUpdateAt: Long, 63 | 64 | @SerializedName("first_install_at") 65 | @Expose 66 | val firstInstallAt: Long, 67 | 68 | @SerializedName("public_key") 69 | @Expose 70 | var publicKey: String? = "" 71 | ) { 72 | 73 | override fun equals(other: Any?): Boolean { 74 | if (this === other) return true 75 | if (javaClass != other?.javaClass) return false 76 | 77 | other as Device 78 | 79 | return tag == other.tag 80 | // Check other properties similarly 81 | // return true if all properties are equal 82 | 83 | } 84 | 85 | override fun hashCode(): Int { 86 | return tag.hashCode() 87 | } 88 | 89 | override fun toString(): String { 90 | return "Device(tag='$tag', name='$name', brand='$brand', os='$os', secret='$secret', manufacturer='$manufacturer', osVersion='$osVersion', token='$token', userName='$userName', appVersionName='$appVersionName', appVersion=$appVersion, lastUpdateAt=$lastUpdateAt, firstInstallAt=$firstInstallAt)" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/data/Message.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.data 2 | 3 | data class Message(var sender: Device, var room: String, var payload: String, var action: String, var type: String, var tag: String, var message: String, var data: String, var created_at: Long) { 4 | override fun toString(): String { 5 | return "Message(sender=$sender, room='$room', tag='$tag', message='$message', created_at=$created_at)" 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/data/Room.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.data 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Room( 7 | @SerializedName("tag") 8 | @Expose 9 | val tag: String = "", 10 | 11 | @SerializedName("stream_link") 12 | @Expose 13 | var streamLink: String? = "", 14 | 15 | @SerializedName("status") 16 | @Expose 17 | val status: String? = "", 18 | 19 | @SerializedName("room_key") 20 | @Expose 21 | var roomKey: String? = "", 22 | 23 | ) { 24 | 25 | override fun equals(other: Any?): Boolean { 26 | if (this === other) return true 27 | if (javaClass != other?.javaClass) return false 28 | 29 | other as Room 30 | 31 | return tag == other.tag 32 | // Check other properties similarly 33 | // return true if all properties are equal 34 | 35 | } 36 | 37 | override fun hashCode(): Int { 38 | return tag.hashCode() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/db/SessionStorage.java: -------------------------------------------------------------------------------- 1 | package com.playsho.android.db; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.playsho.android.base.ApplicationLoader; 7 | import com.playsho.android.utils.Validator; 8 | 9 | /** 10 | * A class for managing session storage using SharedPreferences. 11 | */ 12 | public class SessionStorage { 13 | private final SharedPreferences pref; 14 | private final SharedPreferences.Editor editor; 15 | 16 | // Name of the shared preference file 17 | private final String SHARED_PREFERENCE_NAME = "main_sp"; 18 | 19 | /** 20 | * Constructs a SessionStorage instance and initializes SharedPreferences and its editor. 21 | */ 22 | public SessionStorage() { 23 | this.pref = ApplicationLoader.context.getSharedPreferences( 24 | SHARED_PREFERENCE_NAME, 25 | Context.MODE_PRIVATE 26 | ); 27 | editor = pref.edit(); 28 | editor.apply(); 29 | } 30 | 31 | /** 32 | * Clears all entries in the SharedPreferences. 33 | */ 34 | public void clearAll(){ 35 | pref.edit().clear().apply(); 36 | } 37 | 38 | /** 39 | * Gets a String value from the SharedPreferences with the specified key. 40 | * 41 | * @param key The key for the String value. 42 | * @return The String value associated with the key, or {@code null} if not found. 43 | */ 44 | public String getString(String key) { 45 | return pref.getString(key, null); 46 | } 47 | 48 | 49 | /** 50 | * Gets a String value from the SharedPreferences with the specified key, providing a default value if not found. 51 | * 52 | * @param key The key for the String value. 53 | * @param defValue The default value to return if the key is not found. 54 | * @return The String value associated with the key, or the default value if not found. 55 | */ 56 | public String getString(String key , String defValue) { 57 | return pref.getString(key, defValue); 58 | } 59 | 60 | /** 61 | * Sets a String value in the SharedPreferences with the specified key. 62 | * 63 | * @param key The key for the String value. 64 | * @param value The String value to set. 65 | */ 66 | public void setString(String key, String value) { 67 | editor.putString(key, value); 68 | editor.apply(); 69 | editor.commit(); 70 | } 71 | 72 | /** 73 | * Gets an integer value from the SharedPreferences with the specified key. 74 | * 75 | * @param key The key for the integer value. 76 | * @return The integer value associated with the key, or 0 if not found. 77 | */ 78 | public int getInteger(String key) { 79 | return pref.getInt(key, 0); 80 | } 81 | 82 | /** 83 | * Gets an integer value from the SharedPreferences with the specified key, providing a default value if not found. 84 | * 85 | * @param key The key for the integer value. 86 | * @param defValue The default value to return if the key is not found. 87 | * @return The integer value associated with the key, or the default value if not found. 88 | */ 89 | public int getInteger(String key , int defValue) { 90 | return pref.getInt(key, defValue); 91 | } 92 | 93 | /** 94 | * Sets an integer value in the SharedPreferences with the specified key. 95 | * 96 | * @param key The key for the integer value. 97 | * @param value The integer value to set. 98 | */ 99 | public void setInteger(String key, int value) { 100 | editor.putInt(key, value); 101 | editor.apply(); 102 | editor.commit(); 103 | } 104 | 105 | 106 | /** 107 | * Deserializes a JSON string stored in SharedPreferences into an object of the specified class. 108 | * 109 | * @param key The key for the JSON string. 110 | * @param clazz The class type to deserialize the JSON into. 111 | * @param The type of the class. 112 | * @return An object of the specified class, or a default object if the JSON is not found. 113 | */ 114 | /* public T deserialize(String key, Class clazz) { 115 | String json = this.getString(key); 116 | if (Validator.isNullOrEmpty(json)) { 117 | json = "{}"; 118 | } 119 | return ApplicationLoader.getGson().fromJson(json, clazz); 120 | } 121 | */ 122 | /** 123 | * Inserts a JSON-serializable object into SharedPreferences with the specified key. 124 | * 125 | * @param key The key for storing the JSON string. 126 | * @param o The object to be serialized and stored. 127 | */ 128 | // public void insertJson(String key, Object o) { 129 | // this.editor.putString(key,ApplicationLoader.getGson().toJson(o)); 130 | // editor.apply(); 131 | // editor.commit(); 132 | // } 133 | 134 | 135 | /** 136 | * Gets a boolean value from the SharedPreferences with the specified key. 137 | * 138 | * @param key The key for the boolean value. 139 | * @return The boolean value associated with the key, or {@code false} if not found. 140 | */ 141 | public boolean getBoolean(String key) { 142 | return pref.getBoolean(key, false); 143 | } 144 | 145 | /** 146 | * Gets a boolean value from the SharedPreferences with the specified key, providing a default value if not found. 147 | * 148 | * @param key The key for the boolean value. 149 | * @param defValue The default value to return if the key is not found. 150 | * @return The boolean value associated with the key, or the default value if not found. 151 | */ 152 | public boolean getBoolean(String key, boolean defValue) { 153 | return pref.getBoolean(key, defValue); 154 | } 155 | 156 | /** 157 | * Sets a boolean value in the SharedPreferences with the specified key. 158 | * 159 | * @param key The key for the boolean value. 160 | * @param value The boolean value to set. 161 | */ 162 | public void setBoolean(String key, boolean value) { 163 | editor.putBoolean(key, value); 164 | editor.apply(); 165 | editor.commit(); 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/APIService.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.playsho.android.config.Conf 4 | import okhttp3.RequestBody 5 | import retrofit2.Call 6 | import retrofit2.http.* 7 | 8 | interface APIService { 9 | 10 | @POST(Conf.Path.V1_DEVICE_GENERATE) 11 | fun generateDevice(@Body body: RequestBody): Call 12 | 13 | @POST(Conf.Path.V1_ROOM_CREATE) 14 | fun createRoom(): Call 15 | 16 | @POST(Conf.Path.V1_DEVICE_NAME) 17 | fun updateName(@Body body: RequestBody): Call 18 | 19 | @POST(Conf.Path.V1_DEVICE_KEYPAIR_REGENERATE) 20 | fun regenerateDeviceKeypair(@Body body: RequestBody): Call 21 | 22 | @GET(Conf.Path.V1_ROOM_GET) 23 | fun getRoom( 24 | @Path("tag") tag: String 25 | ): Call 26 | 27 | @GET(Conf.Path.V1_ROOM_ENTRANCE) 28 | fun checkEntrance( 29 | @Path("tag") tag: String 30 | ): Call 31 | 32 | @POST(Conf.Path.V1_ROOM_LINK) 33 | fun addLink( 34 | @Path("tag") tag: String, 35 | @Body body: RequestBody 36 | ): Call 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/Agent.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.playsho.android.config.Conf 4 | import com.playsho.android.utils.DeviceUtils 5 | import okhttp3.FormBody 6 | import okhttp3.RequestBody 7 | import retrofit2.Call 8 | import java.security.PublicKey 9 | 10 | 11 | object Agent { 12 | 13 | object Device { 14 | 15 | fun generate(privateKey: String): Call { 16 | val device = DeviceUtils.createDevice(); 17 | val builder = FormBody.Builder().apply { 18 | device.secret?.let { add(Conf.Query.SECRET, it) } 19 | add(Conf.Query.PUBLIC_KEY, privateKey) 20 | add(Conf.Query.FCM, "not_set") 21 | add(Conf.Query.BRAND, device.brand) 22 | add(Conf.Query.MODEL, device.model) 23 | add(Conf.Query.MANUFACTURER, device.manufacturer) 24 | add(Conf.Query.FIRST_INSTALL_TIME, device.firstInstallAt.toString()) 25 | add(Conf.Query.LAST_UPDATE_TIME, device.lastUpdateAt.toString()) 26 | add(Conf.Query.OS_VER, device.osVersion) 27 | add(Conf.Query.APP_VER_NAME, device.appVersionName) 28 | add(Conf.Query.APP_VER, device.appVersion.toString()) 29 | add(Conf.Query.OS, DeviceUtils.OS) 30 | } 31 | val body: RequestBody = builder.build() 32 | return RetrofitClient.getNetworkConfiguration().generateDevice(body) 33 | } 34 | 35 | fun regenerateKeypair(publicKey: String): Call { 36 | val builder = FormBody.Builder().apply { 37 | add(Conf.Query.PUBLIC_KEY, publicKey) 38 | } 39 | val body: RequestBody = builder.build() 40 | return RetrofitClient.getNetworkConfiguration().regenerateDeviceKeypair(body) 41 | } 42 | 43 | fun updateName(name: String): Call { 44 | val builder = FormBody.Builder().apply { 45 | add(Conf.Query.USER_NAME, name) 46 | } 47 | val body: RequestBody = builder.build() 48 | return RetrofitClient.getNetworkConfiguration().updateName(body) 49 | } 50 | } 51 | 52 | object Room { 53 | fun create(): Call { 54 | return RetrofitClient.getNetworkConfiguration().createRoom() 55 | } 56 | 57 | fun get(roomTag: String): Call { 58 | return RetrofitClient.getNetworkConfiguration().getRoom(roomTag); 59 | } 60 | 61 | fun checkEntrance(roomTag: String): Call { 62 | return RetrofitClient.getNetworkConfiguration().checkEntrance(roomTag); 63 | } 64 | 65 | fun addLink(roomTag: String , link:String): Call { 66 | val builder = FormBody.Builder().apply { 67 | add(Conf.Query.STREAM_LINK, link) 68 | } 69 | val body: RequestBody = builder.build() 70 | return RetrofitClient.getNetworkConfiguration().addLink(roomTag,body); 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/Errors.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Errors( 7 | 8 | @SerializedName("property") 9 | @Expose 10 | val property: String, 11 | 12 | @SerializedName("message") 13 | @Expose 14 | val message: String, 15 | 16 | @SerializedName("value") 17 | @Expose 18 | val value: Any, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/Meta.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Meta( 7 | 8 | @SerializedName("page") 9 | @Expose 10 | val page: Int, 11 | 12 | @SerializedName("limit") 13 | @Expose 14 | val limit: Int, 15 | 16 | @SerializedName("totalPage") 17 | @Expose 18 | val totalPage: Int, 19 | 20 | @SerializedName("count") 21 | @Expose 22 | val count: Int 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/Response.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Response( 7 | 8 | @SerializedName("message") 9 | @Expose 10 | val message: String, 11 | 12 | 13 | @SerializedName("code") 14 | @Expose 15 | val code: Int, 16 | 17 | @SerializedName("result") 18 | @Expose 19 | val result: Result, 20 | 21 | @SerializedName("errors") 22 | @Expose 23 | val errors: List, 24 | 25 | @SerializedName("meta") 26 | @Expose 27 | val meta: Meta 28 | ) { 29 | 30 | override fun toString(): String { 31 | return "Response{" + 32 | "message='$message', " + 33 | "code=$code, " + 34 | "result=$result, " + 35 | "errors=$errors, " + 36 | "meta=$meta" + 37 | '}' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/Result.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | import com.playsho.android.data.Device 6 | import com.playsho.android.data.Room 7 | 8 | data class Result( 9 | 10 | 11 | 12 | @SerializedName("device") 13 | @Expose 14 | val device: Device, 15 | 16 | @SerializedName("room") 17 | @Expose 18 | val room: Room, 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/RetrofitClient.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | import com.google.gson.GsonBuilder 3 | import com.playsho.android.config.Conf 4 | import com.playsho.android.utils.DebugUtils 5 | import com.playsho.android.utils.Validator 6 | import com.playsho.android.utils.accountmanager.AccountInstance 7 | import okhttp3.ConnectionSpec 8 | import okhttp3.OkHttpClient 9 | import okhttp3.Request 10 | import okhttp3.logging.HttpLoggingInterceptor 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.gson.GsonConverterFactory 13 | import retrofit2.converter.scalars.ScalarsConverterFactory 14 | import java.util.concurrent.TimeUnit 15 | 16 | object RetrofitClient { 17 | 18 | private const val REQUEST_READ_TIME_OUT = 15L 19 | private const val REQUEST_CONNECT_TIME_OUT = 15L 20 | // private const val BASE_URL_TEST = "http://192.168.100.110:3000/" 21 | private const val BASE_URL_TEST = "https://5d44-185-107-80-116.ngrok-free.app" 22 | // private const val SOCKET_URL_TEST = "http://192.168.100.110:7777" 23 | private const val SOCKET_URL_TEST = "https://f3e7-185-107-80-116.ngrok-free.app" 24 | private const val BASE_URL = "https://d.digilog.pro/" 25 | 26 | private val CONNECTION_SPECS = listOf( 27 | ConnectionSpec.COMPATIBLE_TLS, 28 | ConnectionSpec.RESTRICTED_TLS, 29 | ConnectionSpec.CLEARTEXT, 30 | ConnectionSpec.MODERN_TLS 31 | ) 32 | 33 | private var instance: Retrofit? = null 34 | 35 | private fun getInstance(): Retrofit { 36 | return instance ?: synchronized(this) { 37 | instance ?: buildRetrofit().also { instance = it } 38 | } 39 | } 40 | 41 | private fun buildRetrofit(): Retrofit { 42 | return Retrofit.Builder() 43 | .baseUrl(getBaseUrl()) 44 | .addConverterFactory(ScalarsConverterFactory.create()) 45 | .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) 46 | .client(getHttpClient()) 47 | .build() 48 | } 49 | 50 | fun getBaseUrl(): String { 51 | return if (DebugUtils.isDebuggable()) BASE_URL_TEST else BASE_URL 52 | } 53 | 54 | fun getSocketBaseUrl(): String { 55 | return SOCKET_URL_TEST 56 | } 57 | 58 | private fun getHttpClient(): OkHttpClient { 59 | val client = OkHttpClient.Builder() 60 | .addInterceptor { chain -> 61 | val requestBuilder: Request.Builder = chain.request().newBuilder() 62 | requestBuilder.addHeader(Conf.HttpHeader.CONTENT_TYPE, "application/json") 63 | if (!Validator.isNull(AccountInstance.getCurrentAccount())) { 64 | requestBuilder.addHeader( 65 | Conf.HttpHeader.AUTHORIZATION, 66 | "Bearer ".plus(AccountInstance.getCurrentAccount() 67 | ?.let { AccountInstance.getAuthToken(it, "Bearer") }) 68 | ) 69 | } 70 | chain.proceed(requestBuilder.build()) 71 | } 72 | .connectionSpecs(CONNECTION_SPECS) 73 | .readTimeout(REQUEST_READ_TIME_OUT, TimeUnit.SECONDS) 74 | .connectTimeout(REQUEST_CONNECT_TIME_OUT, TimeUnit.SECONDS) 75 | if (DebugUtils.isDebuggable()) { 76 | val logging = HttpLoggingInterceptor() 77 | logging.setLevel(HttpLoggingInterceptor.Level.BODY) 78 | client.addInterceptor(logging) 79 | } 80 | return client.build() 81 | } 82 | 83 | fun getNetworkConfiguration(): APIService { 84 | return getInstance().create(APIService::class.java) 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/network/SocketManager.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.network 2 | 3 | import android.util.Log 4 | import com.playsho.android.utils.accountmanager.AccountInstance 5 | import io.socket.client.IO 6 | import io.socket.client.Socket 7 | import org.json.JSONException 8 | import org.json.JSONObject 9 | 10 | object SocketManager { 11 | private const val TAG = "SocketManager" 12 | private var socket: Socket? = null 13 | 14 | object EVENTS { 15 | const val SEND_MESSAGE = "room_msg" 16 | const val NEW_MESSAGE = "new_message" 17 | const val JOINED = "joined" 18 | const val LEFT = "left" 19 | const val NEW_LINK = "new_link" 20 | const val TRADE = "trade" 21 | const val PAUSE = "pause" 22 | const val PLAYER_STATE = "player_state" 23 | } 24 | 25 | @Synchronized 26 | fun initialize(): SocketManager { 27 | if (socket == null) { 28 | synchronized(SocketManager::class.java) { 29 | if (socket == null) { 30 | 31 | val options = IO.Options().apply { 32 | // Set transports to WebSocket only 33 | forceNew = true 34 | query = "token=${AccountInstance.getAuthToken("Bearer")}" 35 | 36 | } 37 | // Create a new socket instance 38 | socket = IO.socket(RetrofitClient.getSocketBaseUrl(), options) 39 | 40 | socket!!.on(Socket.EVENT_CONNECT) { args -> 41 | Log.e(TAG, "EVENT_CONNECT:") 42 | for (element in args) { 43 | println(element) 44 | } 45 | } 46 | 47 | socket!!.on(Socket.EVENT_CONNECT_ERROR) { args -> 48 | Log.e(TAG, "EVENT_CONNECT_ERROR: $args") 49 | for (element in args) { 50 | println(element) 51 | } 52 | } 53 | 54 | socket!!.on(Socket.EVENT_DISCONNECT) { args -> 55 | Log.e(TAG, "EVENT_DISCONNECT: $args") 56 | for (element in args) { 57 | println(element) 58 | } 59 | } 60 | 61 | socket!!.on(Socket.EVENT_DISCONNECT) { args -> 62 | Log.e(TAG, "EVENT_DISCONNECT: $args") 63 | for (element in args) { 64 | println(element) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | return this; 71 | } 72 | 73 | fun joinRoom(room: String) { 74 | val json = JSONObject() 75 | try { 76 | json.put("room", room) 77 | } catch (e: JSONException) { 78 | e.printStackTrace() 79 | } 80 | socket?.emit("join", json) 81 | } 82 | 83 | fun leaveRoom(room: String) { 84 | val json = JSONObject() 85 | try { 86 | json.put("room", room) 87 | } catch (e: JSONException) { 88 | e.printStackTrace() 89 | } 90 | socket?.emit("leave", json) 91 | } 92 | 93 | @Synchronized 94 | fun establish() { 95 | socket?.connect() 96 | } 97 | 98 | @Synchronized 99 | fun close() { 100 | socket?.disconnect() 101 | socket = null 102 | } 103 | 104 | fun on(eventName: String, callback: (Array) -> Unit) { 105 | socket?.on(eventName, callback) 106 | } 107 | 108 | @Synchronized 109 | fun sendMessage(room: String, message: String) { 110 | val json = JSONObject() 111 | try { 112 | json.put("room", room) 113 | json.put("message", message) 114 | } catch (e: JSONException) { 115 | e.printStackTrace() 116 | } 117 | socket?.emit(EVENTS.SEND_MESSAGE, json) 118 | } 119 | 120 | 121 | @Synchronized 122 | fun sendPlayerState(room: String, state: String, data: String = "") { 123 | val json = JSONObject() 124 | try { 125 | json.put("room", room) 126 | json.put("message", state) 127 | if (data.isNotEmpty()) json.put("data", data) 128 | } catch (e: JSONException) { 129 | e.printStackTrace() 130 | } 131 | socket?.emit(EVENTS.PLAYER_STATE, json) 132 | } 133 | 134 | @Synchronized 135 | fun trade(tag: String) { 136 | val json = JSONObject() 137 | try { 138 | val senderJson = JSONObject() 139 | senderJson.put("tag", AccountInstance.getUserData("tag")) 140 | senderJson.put("public_key", AccountInstance.getAuthToken("public_key")) 141 | val receiverJson = JSONObject() 142 | receiverJson.put("tag", tag) 143 | json.put("sender", senderJson) 144 | json.put("receiver", receiverJson) 145 | } catch (e: JSONException) { 146 | e.printStackTrace() 147 | } 148 | socket?.emit(EVENTS.TRADE, json) 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/ui/SettingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.View 6 | import com.google.android.material.snackbar.Snackbar 7 | import com.playsho.android.R 8 | import com.playsho.android.base.BaseActivity 9 | import com.playsho.android.base.BaseBottomSheet 10 | import com.playsho.android.databinding.ActivitySettingBinding 11 | import com.playsho.android.network.Agent 12 | import com.playsho.android.network.Response 13 | import com.playsho.android.ui.bottomsheet.ChangeNameBottomSheet 14 | import com.playsho.android.utils.KeyStoreHelper 15 | import com.playsho.android.utils.LocalController 16 | import com.playsho.android.utils.RSAHelper 17 | import com.playsho.android.utils.accountmanager.AccountInstance 18 | import retrofit2.Call 19 | import retrofit2.Callback 20 | 21 | class SettingActivity : BaseActivity() { 22 | 23 | 24 | override fun getLayoutResourceId(): Int { 25 | return R.layout.activity_setting 26 | } 27 | 28 | override fun onBackPress() { 29 | TODO("Not yet implemented") 30 | } 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setStatusBarColor(R.color.black_background, true) 35 | binding.icBack.setOnClickListener { 36 | finish() 37 | } 38 | binding.containerGenerateKeys.setOnClickListener { 39 | binding.progress.visibility = View.VISIBLE 40 | KeyStoreHelper.getInstance().deleteEntry(KeyStoreHelper.KeyAllies.RSA_KEYS) 41 | 42 | val keys = RSAHelper.generateKeyPair() 43 | // Create a certificate chain containing the public key 44 | 45 | Agent.Device.regenerateKeypair(RSAHelper.printPublicKey(keys)) 46 | .enqueue(object : Callback { 47 | 48 | override fun onFailure(call: Call, t: Throwable) { 49 | binding.progress.visibility = View.GONE 50 | Snackbar.make( 51 | binding.root, 52 | LocalController.getString(R.string.rsa_key_pair_successfully_regenerated), 53 | Snackbar.LENGTH_LONG 54 | ).show() 55 | } 56 | 57 | override fun onResponse( 58 | call: Call, 59 | response: retrofit2.Response 60 | ) { 61 | binding.progress.visibility = View.GONE 62 | Snackbar.make( 63 | binding.root, 64 | response.body()?.message 65 | ?: LocalController.getString(R.string.rsa_key_pair_successfully_regenerated), 66 | Snackbar.LENGTH_LONG 67 | ).show() 68 | } 69 | }) 70 | 71 | } 72 | 73 | binding.containerChangeName.setOnClickListener { 74 | val bottomSheet = ChangeNameBottomSheet() 75 | bottomSheet.setOnResult(callback = object : BaseBottomSheet.BottomSheetResultCallback { 76 | override fun onBottomSheetProcessSuccess(data: String) { 77 | loadData() 78 | Snackbar.make( 79 | binding.root, 80 | LocalController.getString(R.string.name_successfully_updated), 81 | Snackbar.LENGTH_LONG 82 | ).show() 83 | } 84 | 85 | override fun onBottomSheetProcessFail(data: String) { 86 | Snackbar.make( 87 | binding.root, 88 | data, 89 | Snackbar.LENGTH_LONG 90 | ).show() 91 | } 92 | }) 93 | bottomSheet.show(supportFragmentManager, "name") 94 | } 95 | loadData() 96 | } 97 | 98 | @SuppressLint("SetTextI18n") 99 | private fun loadData() { 100 | binding.txtTag.text = "Tag:${AccountInstance.getUserData("tag")}" 101 | binding.txtUserName.text = AccountInstance.getUserData("user_name") 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/ui/bottomsheet/ChangeNameBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.ui.bottomsheet 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.inputmethod.EditorInfo 6 | import com.google.android.material.snackbar.Snackbar 7 | import com.google.gson.Gson 8 | import com.playsho.android.R 9 | import com.playsho.android.base.BaseBottomSheet 10 | import com.playsho.android.databinding.BottomSheetChangeNameBinding 11 | import com.playsho.android.network.Agent 12 | import com.playsho.android.network.Response 13 | import com.playsho.android.utils.SystemUtilities 14 | import com.playsho.android.utils.ThemeHelper 15 | import com.playsho.android.utils.accountmanager.AccountInstance 16 | import retrofit2.Call 17 | import retrofit2.Callback 18 | 19 | class ChangeNameBottomSheet : BaseBottomSheet() { 20 | 21 | lateinit var callback: BottomSheetResultCallback 22 | 23 | override fun getLayoutResourceId(): Int { 24 | return R.layout.bottom_sheet_change_name 25 | } 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | binding.input.setText(AccountInstance.getUserData("user_name")) 30 | binding.input.requestFocus() // Set focus to EditText 31 | binding.input.selectAll() // Select all text in EditText 32 | binding.input.background = ThemeHelper.createRect( 33 | R.color.neutral_100, 34 | 45, 35 | ) 36 | binding.btn.setOnClickListener{ 37 | requestChangeName() 38 | } 39 | binding.input.setOnEditorActionListener { _, actionId, _ -> 40 | if (actionId == EditorInfo.IME_ACTION_DONE) { 41 | SystemUtilities.hideKeyboard(binding.input) 42 | requestChangeName() 43 | // Handle the "Go" action here 44 | // For example, perform some action or submit the form 45 | return@setOnEditorActionListener true // Return true to indicate that the action was handled 46 | } 47 | return@setOnEditorActionListener false // Return false if the action was not handled 48 | } 49 | } 50 | 51 | fun setOnResult(callback: BottomSheetResultCallback){ 52 | this.callback = callback 53 | } 54 | 55 | private fun requestChangeName(){ 56 | SystemUtilities.hideKeyboard(binding.input) 57 | binding.btn.startProgress() 58 | Agent.Device.updateName(binding.input.text.toString()).enqueue(object : 59 | Callback { 60 | 61 | override fun onFailure(call: Call, t: Throwable) { 62 | binding.input.requestFocus() 63 | binding.input.selectAll() 64 | SystemUtilities.showKeyboard(binding.input) 65 | binding.btn.stopProgress() 66 | callback.onBottomSheetProcessFail("") 67 | } 68 | 69 | override fun onResponse( 70 | call: Call, 71 | response: retrofit2.Response 72 | ) { 73 | binding.btn.stopProgress() 74 | if (response.isSuccessful){ 75 | AccountInstance.updateAccountUserData("user_name" , binding.input.text.toString()) 76 | binding.btn.stopProgress() 77 | callback.onBottomSheetProcessSuccess(binding.input.text.toString()) 78 | dismiss() 79 | }else{ 80 | val errorResponse = response.errorBody()?.string()?.let { 81 | Gson().fromJson(it, Response::class.java) 82 | } 83 | Snackbar.make( 84 | dialog?.window?.decorView ?:requireActivity().findViewById(android.R.id.content), 85 | errorResponse?.errors?.getOrNull(0)?.message ?: "Error", 86 | Snackbar.LENGTH_LONG 87 | ).show() 88 | } 89 | 90 | } 91 | }) 92 | } 93 | 94 | override fun initView() { 95 | TODO("Not yet implemented") 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/ui/bottomsheet/SendMessageBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.ui.bottomsheet 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.inputmethod.EditorInfo 6 | import com.playsho.android.R 7 | import com.playsho.android.base.BaseBottomSheet 8 | import com.playsho.android.databinding.BottomSheetSendMessageBinding 9 | import com.playsho.android.utils.SystemUtilities 10 | import com.playsho.android.utils.ThemeHelper 11 | 12 | class SendMessageBottomSheet() : BaseBottomSheet() { 13 | override fun getLayoutResourceId(): Int { 14 | return R.layout.bottom_sheet_send_message 15 | } 16 | lateinit var callback: BottomSheetResultCallback 17 | 18 | var titleArray = arrayOf( 19 | "Spill the Popcorn", 20 | "Meme the Moment", 21 | "Live Commentary (Shhh!)", 22 | "Battle Royale of Thoughts", 23 | "Squad Chat: Activate!", 24 | "Mind Meld with Your Crew", 25 | "The Aftershow Starts Now", 26 | "Is This Scene Even Real? Discuss!", 27 | "Who Needs Subtitles? Talk it Out!", 28 | "Spoiler Alert (Just Kidding)", 29 | "The Chat Awakens", 30 | "Let's Get This Party Started (In the Chat)", 31 | "Don't Panic (But Talk in the Chat)", 32 | "Let's Dish About the Movie", 33 | "Spill the Tea (and Snack Commentary)", 34 | "Shhh... We're Chatting About the Movie" 35 | ) 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | 39 | binding.input.background = ThemeHelper.createRect( 40 | R.color.neutral_100, 41 | 45, 42 | ) 43 | 44 | binding.icSend.setOnClickListener { 45 | if (binding.input.text.toString().trim().isNotEmpty()) { 46 | this.callback.onBottomSheetProcessSuccess(binding.input.text.toString().trim()) 47 | } 48 | dismiss() 49 | } 50 | binding.input.setOnEditorActionListener { _, actionId, _ -> 51 | if (actionId == EditorInfo.IME_ACTION_DONE) { 52 | SystemUtilities.hideKeyboard(binding.input) 53 | if (binding.input.text.toString().trim().isNotEmpty()) { 54 | this.callback.onBottomSheetProcessSuccess(binding.input.text.toString().trim()) 55 | } 56 | dismiss() 57 | return@setOnEditorActionListener true // Return true to indicate that the action was handled 58 | } 59 | return@setOnEditorActionListener false // Return false if the action was not handled 60 | } 61 | } 62 | 63 | fun setOnResult(callback: BottomSheetResultCallback){ 64 | this.callback = callback 65 | } 66 | 67 | override fun initView() { 68 | TODO("Not yet implemented") 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/ui/popup/CinemaSettingPopup.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.ui.popup 2 | 3 | import android.content.Context 4 | import com.playsho.android.R 5 | import com.playsho.android.base.BasePopup 6 | import com.playsho.android.databinding.PopupCinemaSettingBinding 7 | 8 | class CinemaSettingPopup(context: Context) : BasePopup(context) { 9 | override fun getLayoutResourceId(): Int { 10 | return R.layout.popup_cinema_setting 11 | } 12 | 13 | override fun onViewInitialized() { 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/AnimationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.content.ClipDescription 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.view.View 7 | import android.view.animation.AlphaAnimation 8 | import android.view.animation.Animation 9 | import android.view.animation.TranslateAnimation 10 | import com.playsho.android.base.ApplicationLoader 11 | 12 | object AnimationHelper { 13 | 14 | object Duration { 15 | const val DISAPPEAR_MESSAGE = 5000L 16 | const val SLIDE_IN_FROM_BOTTOM = 400L 17 | } 18 | 19 | fun slideInFromBottom(view: View, duration: Long) { 20 | val animation = TranslateAnimation( 21 | Animation.RELATIVE_TO_PARENT, 0.0f, 22 | Animation.RELATIVE_TO_PARENT, 0.0f, 23 | Animation.RELATIVE_TO_PARENT, 1.0f, 24 | Animation.RELATIVE_TO_PARENT, 0.0f 25 | ) 26 | animation.duration = duration 27 | view.startAnimation(animation) 28 | } 29 | 30 | fun fadeIn(view: View, duration: Long) { 31 | val animation = AlphaAnimation(0.0f, 1.0f) 32 | animation.duration = duration 33 | view.startAnimation(animation) 34 | } 35 | 36 | fun fadeOut(view: View, duration: Long) { 37 | val animation = AlphaAnimation(1.0f, 0.0f) 38 | animation.duration = duration 39 | view.startAnimation(animation) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/ClipboardHandler.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.content.ClipDescription 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import com.playsho.android.base.ApplicationLoader 7 | 8 | object ClipboardHandler { 9 | private var clipBoardInstance : ClipboardManager = ApplicationLoader.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 10 | 11 | fun hasTextPlainData():Boolean { 12 | return clipBoardInstance.hasPrimaryClip() && 13 | clipBoardInstance.primaryClipDescription?.hasMimeType( 14 | ClipDescription.MIMETYPE_TEXT_PLAIN 15 | ) == true 16 | } 17 | 18 | fun getIndex(index:Int):String{ 19 | return clipBoardInstance.primaryClip?.getItemAt(index)?.text.toString() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/Crypto.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.util.Base64 4 | import java.security.SecureRandom 5 | import javax.crypto.Cipher 6 | import javax.crypto.spec.IvParameterSpec 7 | import javax.crypto.spec.SecretKeySpec 8 | 9 | object Crypto { 10 | 11 | private const val AES_ALGORITHM = "AES" 12 | private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" 13 | private const val AES_IV_SIZE = 16 // 16 bytes for AES 14 | 15 | private fun generateIV(): ByteArray { 16 | val iv = ByteArray(AES_IV_SIZE) // 16 bytes for AES 17 | SecureRandom().nextBytes(iv) 18 | return iv 19 | } 20 | 21 | private fun hexStringToByteArray(hexString: String): ByteArray { 22 | val len = hexString.length 23 | require(len % 2 == 0) { "Hex string length must be even" } 24 | val data = ByteArray(len / 2) 25 | var i = 0 26 | while (i < len) { 27 | data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character.digit( 28 | hexString[i + 1], 29 | 16 30 | )).toByte() 31 | i += 2 32 | } 33 | return data 34 | } 35 | 36 | fun encryptAES(plainText: String, key: String): String { 37 | val cipher = Cipher.getInstance(TRANSFORMATION) 38 | val secretKeySpec = SecretKeySpec(hexStringToByteArray(key), AES_ALGORITHM) 39 | val iv: ByteArray = generateIV() 40 | val ivParameterSpec = IvParameterSpec(iv) 41 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) 42 | val cipherText = cipher.doFinal(plainText.toByteArray()) 43 | return Base64.encodeToString(iv, Base64.DEFAULT).plus(":").plus( 44 | Base64.encodeToString( 45 | cipherText, 46 | Base64.DEFAULT 47 | ) 48 | ) 49 | } 50 | 51 | fun decryptAES(cipherText: String, key: String): String { 52 | val message = cipherText.split(":") 53 | val cipher = Cipher.getInstance(TRANSFORMATION) 54 | val secretKeySpec = SecretKeySpec(hexStringToByteArray(key), AES_ALGORITHM) 55 | val ivParameterSpec = IvParameterSpec(Base64.decode(message[0], Base64.DEFAULT)) 56 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) 57 | val plainText = cipher.doFinal(Base64.decode(message[1], Base64.DEFAULT)) 58 | return String(plainText) 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/Debouncer.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | 6 | /** 7 | * A utility class for debouncing operations in Android applications. 8 | * 9 | * Debouncing is a technique used to limit the frequency of execution of a particular action or task, 10 | * ensuring that it only happens after a specified delay without any additional triggers. 11 | * 12 | * This class allows you to debounce a Runnable, preventing it from running until a certain amount 13 | * of time has passed without further triggers. It's particularly useful in scenarios where you want 14 | * to delay or prevent rapid, repetitive actions such as button clicks or user input. 15 | */ 16 | class Debouncer(private val delayMillis: Long) { 17 | private val handler = Handler(Looper.getMainLooper()) 18 | private var runnable: Runnable? = null 19 | 20 | /** 21 | * Debounces a Runnable, ensuring it is executed after the specified delay, and canceling 22 | * any previously scheduled executions. 23 | * 24 | * @param runnable The Runnable to be debounced. 25 | */ 26 | fun debounce(runnable: Runnable) { 27 | // Cancel the previous runnable if it exists 28 | cancel() 29 | 30 | this.runnable = runnable 31 | handler.postDelayed(runnable, delayMillis) 32 | } 33 | 34 | /** 35 | * Cancels any pending execution of the debounced Runnable. 36 | */ 37 | fun cancel() { 38 | runnable?.let { 39 | handler.removeCallbacks(it) 40 | runnable = null 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/DebugUtils.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.content.pm.PackageManager 4 | import com.playsho.android.base.ApplicationLoader 5 | 6 | object DebugUtils { 7 | 8 | fun isDebuggable(): Boolean { 9 | return try { 10 | val pm = ApplicationLoader.context.packageManager 11 | val appInfo = pm.getApplicationInfo( 12 | ApplicationLoader.context.packageName, 13 | 0 14 | ) 15 | (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0 16 | } catch (e: PackageManager.NameNotFoundException) { 17 | // Handle exception if package is not found 18 | false 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/DimensionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | import android.content.Context 3 | import android.content.res.Configuration 4 | import android.content.res.Resources 5 | import android.graphics.Point 6 | import android.util.DisplayMetrics 7 | import android.util.Log 8 | import android.util.TypedValue 9 | import android.view.WindowManager 10 | import com.playsho.android.base.ApplicationLoader 11 | import kotlin.math.abs 12 | import kotlin.math.ceil 13 | 14 | /** 15 | * Utility class for handling display dimensions and density conversions. 16 | */ 17 | object DimensionUtils { 18 | 19 | private const val TAG = "DimensionUtils" 20 | 21 | // Display properties 22 | var displaySize = Point() 23 | var firstConfigurationWas: Boolean = false 24 | var usingHardwareInput: Boolean = false 25 | var displayMetrics = DisplayMetrics() 26 | var screenRefreshRate: Float = 60f 27 | var density: Float = 1f 28 | 29 | /** 30 | * Retrieves the height of the display in pixels. 31 | * 32 | * @return The height of the display. 33 | */ 34 | fun getDisplayHeightInPixel(): Int { 35 | return Resources.getSystem().displayMetrics.heightPixels 36 | } 37 | 38 | /** 39 | * Retrieves the width of the display in pixels. 40 | * 41 | * @return The width of the display. 42 | */ 43 | fun getDisplayWidthInPixel(): Int { 44 | return Resources.getSystem().displayMetrics.widthPixels 45 | } 46 | 47 | /** 48 | * Converts density-independent pixels (dp) to pixels (px). 49 | * 50 | * @param value The value in dp to be converted. 51 | * @return The converted value in pixels. 52 | */ 53 | fun dpToPx(value: Float): Int { 54 | if (value == 0f) { 55 | return 0 56 | } 57 | return ceil((density * value).toDouble()).toInt() 58 | } 59 | 60 | /** 61 | * Converts pixels (px) to density-independent pixels (dp). 62 | * 63 | * @param px The value in pixels to be converted. 64 | * @return The converted value in dp. 65 | */ 66 | fun pxToDp(px: Int): Float { 67 | val metrics = ApplicationLoader.context.resources.displayMetrics 68 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, px.toFloat(), metrics) 69 | } 70 | 71 | /** 72 | * Checks and updates the display size based on the given context and configuration. 73 | * 74 | * @param context The context. 75 | * @param newConfiguration The new configuration (can be null). 76 | */ 77 | fun checkDisplaySize(context: Context, newConfiguration: Configuration?) { 78 | try { 79 | density = context.resources.displayMetrics.density 80 | firstConfigurationWas = true 81 | val configuration = newConfiguration ?: context.resources.configuration 82 | usingHardwareInput = configuration.keyboard != Configuration.KEYBOARD_NOKEYS && configuration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO 83 | val manager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? 84 | manager?.defaultDisplay?.let { display -> 85 | display.getMetrics(displayMetrics) 86 | display.getSize(displaySize) 87 | screenRefreshRate = display.refreshRate 88 | } 89 | if (configuration.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) { 90 | val newSize = (configuration.screenWidthDp * density + 0.5f).toInt() 91 | if (abs(displaySize.x - newSize) > 3) { 92 | displaySize.x = newSize 93 | } 94 | } 95 | if (configuration.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { 96 | val newSize = (configuration.screenHeightDp * density + 0.5f).toInt() 97 | if (abs(displaySize.y - newSize) > 3) { 98 | displaySize.y = newSize 99 | } 100 | } 101 | } catch (e: Exception) { 102 | Log.e(TAG, "checkDisplaySize: ", e) 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/KeyStoreHelper.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import java.security.KeyStore 4 | import java.security.PrivateKey 5 | import java.security.PublicKey 6 | import java.security.cert.Certificate 7 | 8 | object KeyStoreHelper { 9 | const val KEY_PROVIDER = "AndroidKeyStore" 10 | private val keyStoreInstance: KeyStore by lazy { 11 | KeyStore.getInstance(KEY_PROVIDER).apply { 12 | loadKeyStore() 13 | } 14 | } 15 | 16 | object KeyAllies { 17 | const val RSA_KEYS = "rsa_key" 18 | } 19 | 20 | fun getInstance(): KeyStore { 21 | return keyStoreInstance 22 | } 23 | 24 | private fun KeyStore.loadKeyStore() { 25 | runCatching { 26 | load(null) 27 | }.onFailure { 28 | // Handle error appropriately 29 | it.printStackTrace() 30 | } 31 | } 32 | 33 | fun containsAlias(keyAlias: String): Boolean { 34 | return try { 35 | getInstance().containsAlias(keyAlias) 36 | } catch (e: Exception) { 37 | // Handle error appropriately 38 | e.printStackTrace() 39 | false 40 | } 41 | } 42 | 43 | inline fun getKey(keyAlias: String, pass: CharArray? = null): T { 44 | return try { 45 | when (T::class) { 46 | PrivateKey::class -> getInstance().getKey(keyAlias, pass) as T 47 | PublicKey::class -> getInstance().getCertificate(keyAlias)?.publicKey as T 48 | Certificate::class -> getInstance().getCertificate(keyAlias) as T 49 | else -> throw IllegalArgumentException("Unsupported key type") 50 | } 51 | } catch (e: Exception) { 52 | // Handle error appropriately 53 | e.printStackTrace() 54 | throw IllegalStateException("Error retrieving key") 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/LocalController.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Typeface 5 | import android.graphics.drawable.Drawable 6 | import android.os.Build 7 | import androidx.annotation.RequiresApi 8 | import androidx.core.content.ContextCompat 9 | import com.playsho.android.base.ApplicationLoader 10 | 11 | /** 12 | * Utility class for accessing local resources and assets. 13 | */ 14 | object LocalController { 15 | 16 | /** 17 | * Retrieves the localized string from the resources. 18 | * 19 | * @param resourceId The resource ID of the string. 20 | * @return The localized string. 21 | */ 22 | fun getString(resourceId: Int): String { 23 | return ApplicationLoader.context.resources.getString(resourceId) 24 | } 25 | 26 | /** 27 | * Retrieves a custom font from the resources using font resource ID. 28 | * 29 | * @param resourceId The resource ID of the font. 30 | * @return The Typeface object representing the custom font. 31 | */ 32 | @RequiresApi(api = Build.VERSION_CODES.O) 33 | fun getFont(resourceId: Int): Typeface { 34 | return ApplicationLoader.context.resources.getFont(resourceId) 35 | } 36 | 37 | /** 38 | * Retrieves a custom font from the assets folder using the font name. 39 | * 40 | * @param fontName The name of the font file. 41 | * @return The Typeface object representing the custom font. 42 | */ 43 | fun getFont(fontName: String): Typeface? { 44 | return try { 45 | Typeface.createFromAsset(ApplicationLoader.context.assets, "fonts/$fontName.ttf") 46 | } catch (e: Exception) { 47 | e.printStackTrace() 48 | null 49 | } 50 | } 51 | 52 | /** 53 | * Retrieves the color from the resources. 54 | * 55 | * @param resource The resource ID of the color. 56 | * @return The color value. 57 | */ 58 | fun getColor(resource: Int): Int { 59 | return ContextCompat.getColor(ApplicationLoader.context, resource) 60 | } 61 | 62 | /** 63 | * Retrieves the dimension value from the resources. 64 | * 65 | * @param resource The resource ID of the dimension. 66 | * @return The dimension value. 67 | */ 68 | fun getDimen(resource: Int): Float { 69 | return ApplicationLoader.context.resources.getDimension(resource) 70 | } 71 | 72 | /** 73 | * Retrieves the dimension pixel size from the resources. 74 | * 75 | * @param resource The resource ID of the dimension pixel size. 76 | * @return The dimension pixel size. 77 | */ 78 | fun getDimensionPixelSize(resource: Int): Int { 79 | return ApplicationLoader.context.resources.getDimensionPixelSize(resource) 80 | } 81 | 82 | /** 83 | * Retrieves the drawable from the resources. 84 | * 85 | * @param resource The resource ID of the drawable. 86 | * @return The Drawable object. 87 | */ 88 | fun getDrawable(resource: Int): Drawable? { 89 | return ContextCompat.getDrawable(ApplicationLoader.context, resource) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/NetworkListener.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import androidx.annotation.NonNull 8 | import com.playsho.android.base.ApplicationLoader 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | 11 | /** 12 | * NetworkListener is a utility class that monitors network availability changes using 13 | * ConnectivityManager.NetworkCallback. It provides methods to initialize the listener, 14 | * check the current network availability, and get real-time updates on network status. 15 | */ 16 | class NetworkListener : ConnectivityManager.NetworkCallback() { 17 | 18 | // ConnectivityManager instance for network monitoring 19 | private val connectivityManager: ConnectivityManager = ApplicationLoader.context 20 | .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 21 | 22 | // AtomicBoolean to ensure thread-safe access to network availability status 23 | val isNetworkAvailable = AtomicBoolean(false) 24 | 25 | /** 26 | * Initializes the NetworkListener by registering it with the ConnectivityManager. 27 | * This method should be called to start monitoring network changes. 28 | */ 29 | fun init() { 30 | // Register the NetworkListener to receive network status callbacks 31 | connectivityManager.registerDefaultNetworkCallback(this) 32 | } 33 | 34 | /** 35 | * Checks the current network availability status. 36 | * 37 | * @return True if the network is available, false otherwise. 38 | */ 39 | fun checkNetworkAvailability(): Boolean { 40 | // Obtain the currently active network 41 | val network: Network? = connectivityManager.activeNetwork 42 | 43 | if (network == null) { 44 | // No active network, set availability to false 45 | isNetworkAvailable.set(false) 46 | return isNetworkAvailable() 47 | } 48 | 49 | // Obtain the network capabilities of the active network 50 | val networkCapabilities: NetworkCapabilities? = connectivityManager.getNetworkCapabilities(network) 51 | 52 | if (networkCapabilities == null) { 53 | // Network capabilities not available, set availability to false 54 | isNetworkAvailable.set(false) 55 | return isNetworkAvailable() 56 | } 57 | 58 | // Check if the network has any of the specified transport types (e.g., WiFi, Cellular) 59 | isNetworkAvailable.set(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 60 | || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) 61 | || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) 62 | || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) 63 | 64 | return isNetworkAvailable() 65 | } 66 | 67 | /** 68 | * Gets the real-time network availability status. 69 | * 70 | * @return True if the network is available, false otherwise. 71 | */ 72 | fun isNetworkAvailable(): Boolean { 73 | return isNetworkAvailable.get() 74 | } 75 | 76 | /** 77 | * Called when a network becomes available. 78 | * 79 | * @param network The Network object representing the available network. 80 | */ 81 | override fun onAvailable(network: Network) { 82 | // Set network availability status to true 83 | isNetworkAvailable.set(true) 84 | } 85 | 86 | /** 87 | * Called when a network is lost or becomes unavailable. 88 | * 89 | * @param network The Network object representing the lost network. 90 | */ 91 | override fun onLost(network: Network) { 92 | // Set network availability status to false 93 | isNetworkAvailable.set(false) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/PlayerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import kotlinx.coroutines.DelicateCoroutinesApi 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | import java.net.URL 8 | import java.net.URLConnection 9 | 10 | object PlayerUtils { 11 | 12 | @OptIn(DelicateCoroutinesApi::class) 13 | fun getUrlMimeType(url: String, callback: (String?) -> Unit) { 14 | GlobalScope.launch(Dispatchers.IO) { 15 | try { 16 | val connection = URL(url).openConnection() 17 | val mimeType = connection.contentType 18 | callback(mimeType) 19 | } catch (e: Exception) { 20 | callback(null) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/RSAHelper.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import android.util.Base64 6 | import java.nio.charset.StandardCharsets 7 | import java.security.KeyFactory 8 | import java.security.KeyPair 9 | import java.security.KeyPairGenerator 10 | import java.security.NoSuchAlgorithmException 11 | import java.security.PrivateKey 12 | import java.security.PublicKey 13 | import java.security.spec.InvalidKeySpecException 14 | import java.security.spec.PKCS8EncodedKeySpec 15 | import java.security.spec.X509EncodedKeySpec 16 | import javax.crypto.Cipher 17 | 18 | object RSAHelper { 19 | private const val CRYPTO_METHOD = "RSA" 20 | 21 | fun generateKeyPair(): KeyPair { 22 | val keyPairGenerator = KeyPairGenerator.getInstance(CRYPTO_METHOD, KeyStoreHelper.KEY_PROVIDER) 23 | val spec = KeyGenParameterSpec.Builder( 24 | KeyStoreHelper.KeyAllies.RSA_KEYS, 25 | KeyProperties.PURPOSE_SIGN 26 | or KeyProperties.PURPOSE_VERIFY 27 | or KeyProperties.PURPOSE_ENCRYPT 28 | or KeyProperties.PURPOSE_DECRYPT 29 | ) 30 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) 31 | .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) 32 | .setDigests(KeyProperties.DIGEST_SHA256) 33 | .setKeySize(2048) 34 | .build() 35 | keyPairGenerator.initialize(spec) 36 | return keyPairGenerator.generateKeyPair() 37 | } 38 | 39 | fun getKeyPairs(): KeyPair { 40 | 41 | return if (!KeyStoreHelper.containsAlias(KeyStoreHelper.KeyAllies.RSA_KEYS)) { 42 | // Generate a new RSA key pair 43 | generateKeyPair() 44 | } else { 45 | // Load the existing RSA key pair 46 | val privateKey:PrivateKey = KeyStoreHelper.getKey(KeyStoreHelper.KeyAllies.RSA_KEYS) 47 | val publicKey:PublicKey = KeyStoreHelper.getKey(KeyStoreHelper.KeyAllies.RSA_KEYS) 48 | KeyPair(publicKey, privateKey) 49 | } 50 | } 51 | 52 | /*** Encrypt with Public Key ***/ 53 | fun encrypt( 54 | textToEncrypt: String, 55 | publicKey: PublicKey 56 | ): String { 57 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") 58 | cipher.init(Cipher.ENCRYPT_MODE, publicKey) 59 | val encryptedBytes = cipher.doFinal(textToEncrypt.toByteArray(StandardCharsets.UTF_8)) 60 | return Base64.encodeToString(encryptedBytes, Base64.DEFAULT) 61 | } 62 | 63 | /*** Encrypt with String Public Key ***/ 64 | fun encrypt( 65 | textToEncrypt: String, 66 | publicKeyString: String 67 | ): String { 68 | val publicKey = stringToPublicKey(publicKeyString) 69 | return encrypt( 70 | textToEncrypt = textToEncrypt, 71 | publicKey = publicKey 72 | ) 73 | } 74 | 75 | fun decrypt( 76 | encryptedText: String, 77 | privateKey: PrivateKey 78 | ): String { 79 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") 80 | cipher.init(Cipher.DECRYPT_MODE, privateKey) 81 | val decryptedBytes = cipher.doFinal(Base64.decode(encryptedText, Base64.DEFAULT)) 82 | return String(decryptedBytes) 83 | } 84 | 85 | 86 | /*** Prints Public Key String ***/ 87 | fun printPublicKey(keyPairMap: KeyPair): String { 88 | val pemFormat = StringBuilder() 89 | pemFormat.append("-----BEGIN PUBLIC KEY-----\n") 90 | pemFormat.append(String(Base64.encode(keyPairMap.public.encoded, Base64.DEFAULT))) 91 | pemFormat.append("-----END PUBLIC KEY-----") 92 | return pemFormat.toString() 93 | } 94 | 95 | /*** Converts String Public Key to PublicKey Object ***/ 96 | @Throws(InvalidKeySpecException::class, NoSuchAlgorithmException::class) 97 | private fun stringToPublicKey(publicKeyString: String): PublicKey { 98 | val keyBytes: ByteArray = Base64.decode(publicKeyString, Base64.DEFAULT) 99 | val spec = X509EncodedKeySpec(keyBytes) 100 | val keyFactory = KeyFactory.getInstance(CRYPTO_METHOD) 101 | return keyFactory.generatePublic(spec) 102 | } 103 | 104 | /*** Converts String Private Key to PrivateKey Object ***/ 105 | @Throws(InvalidKeySpecException::class, NoSuchAlgorithmException::class) 106 | private fun stringToPrivateKey(privateKeyString: String): PrivateKey { 107 | val pkcs8EncodedBytes: ByteArray = Base64.decode(privateKeyString, Base64.DEFAULT) 108 | val keySpec = PKCS8EncodedKeySpec(pkcs8EncodedBytes) 109 | val kf = KeyFactory.getInstance(CRYPTO_METHOD) 110 | return kf.generatePrivate(keySpec) 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/ThemeHelper.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.graphics.drawable.GradientDrawable 4 | import androidx.annotation.ColorRes 5 | import androidx.core.content.ContextCompat 6 | import com.playsho.android.base.ApplicationLoader 7 | 8 | /** 9 | * Helper class for creating GradientDrawable objects with various shapes and styles. 10 | */ 11 | object ThemeHelper { 12 | 13 | /** 14 | * Creates a rectangular GradientDrawable with the specified color and radius. 15 | * 16 | * @param color The color resource ID for the rectangle. 17 | * @param radius The corner radius in dp for the rectangle. 18 | * @return A rectangular GradientDrawable. 19 | */ 20 | fun createRect(@ColorRes color: Int, radius: Int): GradientDrawable { 21 | val gradientDrawable = GradientDrawable() 22 | gradientDrawable.shape = GradientDrawable.RECTANGLE 23 | gradientDrawable.cornerRadius = DimensionUtils.dpToPx(radius.toFloat()).toFloat() 24 | gradientDrawable.setColor(ContextCompat.getColor(ApplicationLoader.context, color)) 25 | return gradientDrawable 26 | } 27 | 28 | /** 29 | * Creates a rectangular GradientDrawable with the specified color, radius, stroke color, and stroke width. 30 | * 31 | * @param color The color resource ID for the rectangle. 32 | * @param radius The corner radius in dp for the rectangle. 33 | * @param strokeColor The stroke color resource ID for the rectangle. 34 | * @param strokeWidth The stroke width in dp for the rectangle. 35 | * @return A rectangular GradientDrawable with stroke. 36 | */ 37 | fun createRect(@ColorRes color: Int, radius: Int, @ColorRes strokeColor: Int, strokeWidth: Int): GradientDrawable { 38 | val gradientDrawable = createRect(color, radius) 39 | gradientDrawable.setStroke( 40 | DimensionUtils.dpToPx(strokeWidth.toFloat()).toInt(), 41 | ContextCompat.getColor(ApplicationLoader.context, strokeColor) 42 | ) 43 | 44 | return gradientDrawable 45 | } 46 | 47 | fun createRect( 48 | @ColorRes color: Int, 49 | radius: Int, 50 | @ColorRes strokeColor: Int, 51 | strokeWidth: Int, 52 | dashWidth:Float, 53 | dashGap:Float, 54 | ): GradientDrawable { 55 | val gradientDrawable = createRect(color, radius) 56 | gradientDrawable.setStroke( 57 | DimensionUtils.dpToPx(strokeWidth.toFloat()), 58 | ContextCompat.getColor(ApplicationLoader.context, strokeColor), 59 | DimensionUtils.dpToPx(dashWidth).toFloat(), 60 | DimensionUtils.dpToPx(dashGap).toFloat() 61 | ) 62 | 63 | return gradientDrawable 64 | } 65 | 66 | /** 67 | * Creates a circular GradientDrawable with the specified color. 68 | * 69 | * @param color The color resource ID for the circle. 70 | * @return A circular GradientDrawable. 71 | */ 72 | fun createCircle(@ColorRes color: Int): GradientDrawable { 73 | val gradientDrawable = GradientDrawable() 74 | gradientDrawable.shape = GradientDrawable.OVAL 75 | gradientDrawable.setColor(ContextCompat.getColor(ApplicationLoader.context, color)) 76 | return gradientDrawable 77 | } 78 | 79 | /** 80 | * Creates a circular GradientDrawable with the specified color, stroke color, and stroke width. 81 | * 82 | * @param color The color resource ID for the circle. 83 | * @param strokeColor The stroke color resource ID for the circle. 84 | * @param strokeWidth The stroke width in dp for the circle. 85 | * @return A circular GradientDrawable with stroke. 86 | */ 87 | fun createCircle(@ColorRes color: Int, @ColorRes strokeColor: Int, strokeWidth: Int): GradientDrawable { 88 | val gradientDrawable = createCircle(color) 89 | gradientDrawable.setStroke( 90 | DimensionUtils.dpToPx(strokeWidth.toFloat()).toInt(), 91 | ContextCompat.getColor(ApplicationLoader.context, strokeColor) 92 | ) 93 | return gradientDrawable 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/Validator.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils 2 | 3 | import android.util.Patterns 4 | 5 | /** 6 | * A utility class for performing various validation checks on strings and values. 7 | */ 8 | object Validator { 9 | 10 | /** 11 | * Checks if a string is null or empty. 12 | * 13 | * @param string The string to check. 14 | * @return `true` if the string is null or empty, `false` otherwise. 15 | */ 16 | fun isNullOrEmpty(string: String?): Boolean { 17 | return string.isNullOrEmpty() 18 | } 19 | 20 | 21 | fun isUrl(text: String): Boolean { 22 | val pattern = Patterns.WEB_URL 23 | val matcher = pattern.matcher(text) 24 | return matcher.matches() 25 | } 26 | 27 | /** 28 | * Checks if a reference is null. 29 | * 30 | * @param reference The reference to check. 31 | * @return `true` if the reference is null, `false` otherwise. 32 | */ 33 | fun isNull(reference: T?): Boolean { 34 | return reference == null 35 | } 36 | 37 | /** 38 | * Checks if a string is a valid Iran phone number. 39 | * 40 | * @param phone The phone number to check. 41 | * @return `true` if the phone number is a valid Iran phone number, `false` otherwise. 42 | */ 43 | fun isIranPhoneNumber(phone: String?): Boolean { 44 | return !phone.isNullOrEmpty() && (phone.startsWith("09") || phone.startsWith("+98")) && phone.length >= 11 45 | } 46 | 47 | /** 48 | * Checks if a string has a specific length. 49 | * 50 | * @param content The string to check. 51 | * @param length The expected length. 52 | * @return `true` if the string has the specified length, `false` otherwise. 53 | */ 54 | fun hasLength(content: String?, length: Int): Boolean { 55 | return !content.isNullOrEmpty() && content.length == length 56 | } 57 | 58 | /** 59 | * Checks if the length of a string is greater than a specified value. 60 | * 61 | * @param content The string to check. 62 | * @param length The minimum length. 63 | * @return `true` if the string length is greater than the specified value, `false` otherwise. 64 | */ 65 | fun isGreaterThan(content: String?, length: Int): Boolean { 66 | return !content.isNullOrEmpty() && content.length > length 67 | } 68 | 69 | /** 70 | * Gets the string value or a default if it is null or empty. 71 | * 72 | * @param s The string to check. 73 | * @param def The default value. 74 | * @return The original string if not null or empty, otherwise the default value. 75 | */ 76 | fun getDefaultIfNullOrEmpty(s: String?, def: String): String { 77 | return s ?: def 78 | } 79 | 80 | /** 81 | * Checks if the length of a string is smaller than a specified value. 82 | * 83 | * @param content The string to check. 84 | * @param length The maximum length. 85 | * @return `true` if the string length is smaller than the specified value, `false` otherwise. 86 | */ 87 | fun isSmallerThan(content: String?, length: Int): Boolean { 88 | return !content.isNullOrEmpty() && content.length < length 89 | } 90 | 91 | /** 92 | * Checks if two integers are equal. 93 | * 94 | * @param a The first integer. 95 | * @param b The second integer. 96 | * @return `true` if the integers are equal, `false` otherwise. 97 | */ 98 | fun isEqual(a: Int, b: Int): Boolean { 99 | return a == b 100 | } 101 | 102 | /** 103 | * Checks if two strings are equal (handling null values). 104 | * 105 | * @param a The first string. 106 | * @param b The second string. 107 | * @return `true` if the strings are equal, `false` otherwise. 108 | */ 109 | fun isEqual(a: String?, b: String?): Boolean { 110 | return a == b 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/accountmanager/AccountAuthenticator.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils.accountmanager 2 | 3 | import android.accounts.AbstractAccountAuthenticator 4 | import android.accounts.Account 5 | import android.accounts.AccountAuthenticatorResponse 6 | import android.accounts.NetworkErrorException 7 | import android.content.Context 8 | import android.os.Bundle 9 | 10 | /** 11 | * Custom Account Authenticator for managing user accounts. 12 | * 13 | * This class extends AbstractAccountAuthenticator and is responsible for handling various account-related 14 | * operations such as account creation, authentication, token retrieval, and more. 15 | * 16 | * Note: This class provides a skeleton implementation, and the actual functionality should be implemented 17 | * based on the specific requirements of your application. 18 | * 19 | * @see AbstractAccountAuthenticator 20 | */ 21 | class AccountAuthenticator(context: Context) : AbstractAccountAuthenticator(context) { 22 | 23 | /** 24 | * Allows editing properties for the account. 25 | * 26 | * @param response The response to send the result back to the AccountManager. 27 | * @param accountType The account type. 28 | * @return A Bundle containing any additional information. 29 | */ 30 | override fun editProperties(response: AccountAuthenticatorResponse, accountType: String): Bundle? { 31 | // Implement if needed 32 | return null 33 | } 34 | 35 | /** 36 | * Adds a new account to the system. 37 | * 38 | * @param response The response to send the result back to the AccountManager. 39 | * @param accountType The account type. 40 | * @param authTokenType The authentication token type. 41 | * @param requiredFeatures An array of features the account must support. 42 | * @param options A Bundle containing additional options. 43 | * @return A Bundle containing the result of the account creation operation. 44 | * @throws NetworkErrorException If a network error occurs. 45 | */ 46 | @Throws(NetworkErrorException::class) 47 | override fun addAccount( 48 | response: AccountAuthenticatorResponse, 49 | accountType: String, 50 | authTokenType: String?, 51 | requiredFeatures: Array?, 52 | options: Bundle? 53 | ): Bundle? { 54 | // Implement account creation here 55 | return null 56 | } 57 | 58 | override fun confirmCredentials( 59 | response: AccountAuthenticatorResponse?, 60 | account: Account?, 61 | options: Bundle? 62 | ): Bundle { 63 | TODO("Not yet implemented") 64 | } 65 | 66 | override fun getAuthToken( 67 | response: AccountAuthenticatorResponse?, 68 | account: Account?, 69 | authTokenType: String?, 70 | options: Bundle? 71 | ): Bundle { 72 | TODO("Not yet implemented") 73 | } 74 | 75 | override fun getAuthTokenLabel(authTokenType: String?): String { 76 | TODO("Not yet implemented") 77 | } 78 | 79 | override fun updateCredentials( 80 | response: AccountAuthenticatorResponse?, 81 | account: Account?, 82 | authTokenType: String?, 83 | options: Bundle? 84 | ): Bundle { 85 | TODO("Not yet implemented") 86 | } 87 | 88 | override fun hasFeatures( 89 | response: AccountAuthenticatorResponse?, 90 | account: Account?, 91 | features: Array? 92 | ): Bundle { 93 | TODO("Not yet implemented") 94 | } 95 | 96 | // Implement other methods similarly 97 | 98 | // Other methods have similar implementations 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/accountmanager/AccountAuthenticatorService.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils.accountmanager 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | import android.support.annotation.Nullable 7 | import com.playsho.android.base.ApplicationLoader 8 | 9 | /** 10 | * Service responsible for managing the Account Authenticator. 11 | * 12 | * This service extends the Android Service class and acts as a bridge between the Android 13 | * AccountManager and the custom AccountAuthenticator. It provides the IBinder to the 14 | * AccountAuthenticator, allowing the AccountManager to interact with it. 15 | */ 16 | class AccountAuthenticatorService : Service() { 17 | 18 | /** 19 | * The instance of the custom AccountAuthenticator. 20 | */ 21 | private val authenticator: AccountAuthenticator = AccountAuthenticator(ApplicationLoader.context) 22 | 23 | /** 24 | * Called when the service is created. Perform one-time initialization here. 25 | */ 26 | override fun onCreate() { 27 | super.onCreate() 28 | // Perform any additional initialization if needed 29 | } 30 | 31 | /** 32 | * Called when the service is bound by the AccountManager. 33 | * 34 | * @param intent The intent that was used to bind to this service. 35 | * @return An IBinder interface to the service. 36 | */ 37 | @Nullable 38 | override fun onBind(intent: Intent): IBinder? { 39 | /* 40 | * Return the IBinder interface to the AccountAuthenticator. 41 | * This allows the AccountManager to interact with the AccountAuthenticator. 42 | */ 43 | return authenticator.iBinder 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/playsho/android/utils/accountmanager/AccountInstance.kt: -------------------------------------------------------------------------------- 1 | package com.playsho.android.utils.accountmanager 2 | 3 | import com.playsho.android.base.ApplicationLoader 4 | 5 | import android.accounts.Account 6 | import android.accounts.AccountManager 7 | import android.os.Bundle 8 | /** 9 | * The `AccountInstance` class provides utility methods for managing user accounts 10 | * using the Android `AccountManager`. 11 | */ 12 | object AccountInstance { 13 | 14 | private val ACCOUNT_TYPE = ApplicationLoader.context.packageName 15 | private val accountManager: AccountManager = AccountManager.get(ApplicationLoader.context) 16 | private var currentAccount: Account? = null 17 | 18 | /** 19 | * Set the current active account. 20 | * 21 | * @param account The account to set as the current active account. 22 | */ 23 | fun use(account: Account?) { 24 | currentAccount = account 25 | } 26 | 27 | /** 28 | * Get the current active account. 29 | * 30 | * @return The current active account. 31 | */ 32 | fun getCurrentAccount(): Account? { 33 | return currentAccount 34 | } 35 | 36 | /** 37 | * Check if there is any account available. 38 | * 39 | * @return `true` if there is at least one account, otherwise `false`. 40 | */ 41 | fun hasAnyAccount(): Boolean { 42 | return getAccounts().isNotEmpty() 43 | } 44 | 45 | /** 46 | * Update user data associated with a specific account. 47 | * 48 | * @param account The account to update user data for. 49 | * @param key The key for the user data. 50 | * @param value The new value for the user data. 51 | */ 52 | fun updateAccountUserData(account: Account, key: String, value: String) { 53 | accountManager.setUserData(account, key, value) 54 | } 55 | 56 | /** 57 | * Update user data associated with the current active account. 58 | * 59 | * @param key The key for the user data. 60 | * @param value The new value for the user data. 61 | */ 62 | fun updateAccountUserData(key: String, value: String) { 63 | currentAccount?.let { accountManager.setUserData(it, key, value) } 64 | } 65 | 66 | /** 67 | * Get user data associated with a specific account. 68 | * 69 | * @param account The account to get user data for. 70 | * @param key The key for the user data. 71 | * @return The value of the user data. 72 | */ 73 | fun getUserData(account: Account, key: String): String? { 74 | return accountManager.getUserData(account, key) 75 | } 76 | 77 | /** 78 | * Get user data associated with the current active account. 79 | * 80 | * @param key The key for the user data. 81 | * @return The value of the user data. 82 | */ 83 | fun getUserData(key: String): String? { 84 | return currentAccount?.let { accountManager.getUserData(it, key) } 85 | } 86 | 87 | /** 88 | * Get the authentication token for a specific account and token type. 89 | * 90 | * @param account The account to get the authentication token for. 91 | * @param tokenType The type of the authentication token. 92 | * @return The authentication token. 93 | */ 94 | fun getAuthToken(account: Account, tokenType: String): String? { 95 | return accountManager.peekAuthToken(account, tokenType) 96 | } 97 | 98 | /** 99 | * Get the authentication token for the current active account and token type. 100 | * 101 | * @param tokenType The type of the authentication token. 102 | * @return The authentication token. 103 | */ 104 | fun getAuthToken(tokenType: String): String { 105 | return currentAccount.let { accountManager.peekAuthToken(it, tokenType) } 106 | } 107 | 108 | /** 109 | * Initialize the `AccountInstance` by setting the first available account as the current active account. 110 | */ 111 | fun init() { 112 | val accounts = getAccounts() 113 | if (accounts.isNotEmpty()) { 114 | use(accounts[0]) 115 | } 116 | } 117 | 118 | /** 119 | * Get an array of all accounts of the specified type. 120 | * 121 | * @return An array of accounts. 122 | */ 123 | fun getAccounts(): Array { 124 | return accountManager.getAccountsByType(ACCOUNT_TYPE) 125 | } 126 | 127 | /** 128 | * Remove an account by name. 129 | * 130 | * @param accountName The name of the account to remove. 131 | */ 132 | fun removeAccount(accountName: String) { 133 | getAccounts().forEach { account -> 134 | if (account.name == accountName) { 135 | accountManager.removeAccountExplicitly(account) 136 | return@forEach 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Set the authentication token for a specific account and token type. 143 | * 144 | * @param account The account to set the authentication token for. 145 | * @param tokenType The type of the authentication token. 146 | * @param authToken The authentication token. 147 | */ 148 | fun setAuthToken(account: Account, tokenType: String, authToken: String) { 149 | accountManager.setAuthToken(account, tokenType, authToken) 150 | } 151 | 152 | /** 153 | * Set the authentication token for the current active account and token type. 154 | * 155 | * @param tokenType The type of the authentication token. 156 | * @param authToken The authentication token. 157 | */ 158 | fun setAuthToken(tokenType: String, authToken: String) { 159 | currentAccount?.let { accountManager.setAuthToken(it, tokenType, authToken) } 160 | } 161 | 162 | /** 163 | * Create a new account with the specified name, password, and optional bundle. 164 | * 165 | * @param name The name of the new account. 166 | * @param pass The password for the new account. 167 | * @param bundle An optional bundle of account details. 168 | * @return The newly created account. 169 | */ 170 | fun createAccount(name: String, pass: String, bundle: Bundle?): Account { 171 | val account = Account(name, ACCOUNT_TYPE) 172 | accountManager.addAccountExplicitly(account, pass, bundle) 173 | return account 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_eye.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_left_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_left_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_logo_mini.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_message.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_send_rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_send_rocket.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_smiley.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-hdpi/ic_action_youtube.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_eye.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_left_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_left_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_logo_mini.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_message.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_send_rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_send_rocket.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_smiley.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-mdpi/ic_action_youtube.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/img_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-nodpi/img_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_eye.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_left_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_left_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_logo_mini.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_message.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_send_rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_send_rocket.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_smiley.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xhdpi/ic_action_youtube.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_eye.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_left_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_left_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_logo_mini.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_message.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_send_rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_send_rocket.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_smiley.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxhdpi/ic_action_youtube.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_eye.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_left_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_left_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_logo_mini.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_message.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_send_rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_send_rocket.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_smiley.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/drawable-xxxhdpi/ic_action_youtube.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_top_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chat_bubble_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chat_bubble_sender.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chat_bubble_system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/font/roboto_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/font/roboto_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/font/roboto_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_cinema.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 26 | 27 | 35 | 36 | 37 | 38 | 44 | 45 | 67 | 68 | 69 | 74 | 75 | 86 | 87 | 94 | 95 | 105 | 106 | 107 | 108 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_room.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | 28 | 29 | 37 | 38 | 45 | 46 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 74 | 75 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 97 | 98 | 99 | 108 | 109 | 119 | 120 | 133 | 134 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 22 | 23 | 31 | 32 | 38 | 39 | 46 | 47 | 48 | 49 | 62 | 63 | 75 | 76 | 88 | 89 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_sheet_add_link.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 19 | 20 | 29 | 30 | 46 | 47 | 48 | 57 | 58 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_sheet_change_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 23 | 24 | 40 | 41 | 42 | 51 | 52 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_sheet_join_room.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 24 | 25 | 41 | 42 | 43 | 52 | 53 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/bottom_sheet_send_message.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 24 | 25 | 35 | 36 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_message_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_message_sender.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 20 | 21 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_message_system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_cinema_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryb3ast/playsho/8142bd8f5b25c495db1f71328d0fff31d7d44eb2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #181B22 4 | #EEEFF0 5 | #F2F2F2 6 | #81858B 7 | #FFFFFF 8 | #0FABC6 9 | #0FABC6 10 | #E0E0E2 11 | #767981 12 | #254FC3 13 | #ffffff 14 | #767981 15 | #F7F7F8 16 | #CDCED1 17 | #777777 18 | #323641 19 | #1E222A 20 | #254FC3 21 | #424750 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56dp 4 | 24dp 5 | 100dp 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #181B22 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Playsho 3 | Play and watch together 4 | Create a room 5 | Add friends 6 | Add stream link to play 7 | Type a message or link 8 | Add video link 9 | Paste or type video link 10 | You can add any video link from any website with stream support. 11 | Add and Play! 12 | 1- Create a room without any hassle!\n2- Drop in a video link like a pro!\n3- Bring your buddies onboard!\n4- Sit back, relax, and enjoy the show! 🍿✨ :) 13 | Play and watch togather 14 | Create a room 15 | 1- Create a room without any hassle!\n2- Drop in a video link like a pro!\n3- Bring your buddies onboard!\n4- Sit back, relax, and enjoy the show! 🍿✨ :) 16 | + Add friends 17 | + Add stream link to play 18 | Type a message or link 19 | Loading... 20 | Join Room 21 | Room code: Paste or type it here! 🍿 22 | You\'re just one step away from joining the fun. Simply paste or type your room code into the box below. Need help finding it? Look for the unique combination of letters and numbers in your invite link, then enter only the code part here. Let\'s dive into the movie magic together! 23 | Join 24 | Settings 25 | Account 26 | Your end-to-end encryption key 27 | Regenerate Keys 28 | Sign out 29 | Sign out from your account and register again 30 | 🔑 RSA key pair successfully regenerated! 🎉 31 | Ta-da! Your name has been successfully updated! 32 | 🔑 Oh no! Something went wrong while regenerating the RSA key pair. 🤯 Please try again later and we\'ll get it sorted out! 🛠️ 33 | Enter your name (e.g., John Doe) 34 | We\'d love to get to know you better. 😊 35 | Save 36 | Hey there! Want to level up your experience with us? Throw in your full name! We promise to sprinkle some magic and make your time with us as awesome as a unicorn\'s birthday party! 🦄✨ 37 | Oops! Looks like the room tag is missing. Let\'s fill in that blank space! 38 | Paste the stream link here 39 | Ready for some streaming shenanigans? Drop your stream link into the magic box below and let\'s kick off the ultimate movie night! Don\'t keep the flicks waiting, share the fun with your roomies now! 40 | Submit 41 | Hey there! It looks like you haven\'t entered a valid stream link. Give it another shot! 42 | Loading... 43 | Chat 44 | Send 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 25 | 26 | 34 | 35 | 36 | 44 | 45 | 53 | 54 | 62 | 63 | 71 | 72 | 80 | 81 | 89 | 90 | 98 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |