🔓 Kotlin version of the popular google/easypermissions wrapper library to simplify basic system permissions logic on Android M or higher.

Overview

EasyPermissions-ktx

Build Status Code Coverage Latest Version Android API Kotlin Weekly Android Weekly Apache License

Kotlin version of the popular googlesample/easypermissions wrapper library to simplify basic system permissions logic on Android M or higher.

Logo

This library lifts the burden that comes with writing a bunch of check statements whether a permission has been granted or not from you, in order to keep your code clean and safe.

Installation

EasyPermissions-ktx is installed by adding the following dependency to your build.gradle file:

dependencies {
    implementation 'com.vmadalin:easypermissions-ktx:1.0.0'
}

Tutorial

This video tutorial helps and guide you regarding all the process to integrate the library to your project and configure it, thanks to Stevdza-San.

Usage

Basic

To begin using EasyPermissions-ktx, have your Activity (or Fragment) override the onRequestPermissionsResult method:

class MainActivity : AppCompatActivity() {

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        // EasyPermissions handles the request result.
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    }
}

Request Permissions

The example below shows how to request permissions for a method that requires both CAMERA and ACCESS_FINE_LOCATION permissions. There are a few things to note:

  • Using EasyPermissions#hasPermissions(...) to check if the app already has the required permissions. This method can take any number of permissions as its final argument.
  • Requesting permissions with EasyPermissions#requestPermissions. This method will request the system permissions and show the rationale string provided if necessary. The request code provided should be unique to this request, and the method can take any number of permissions as its final argument.
  • Use of the AfterPermissionGranted annotation. This is optional, but provided for convenience. If all of the permissions in a given request are granted, all methods annotated with the proper request code will be executed(be sure to have an unique request code). The annotated method needs to be void and without input parameters (instead, you can use onSaveInstanceState in order to keep the state of your suppressed parameters). This is to simplify the common flow of needing to run the requesting method after all of its permissions have been granted. This can also be achieved by adding logic on the onPermissionsGranted callback.
@AfterPermissionGranted(REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION)
private void methodRequiresTwoPermission() {
    if (EasyPermissions.hasPermissions(this, ACCESS_FINE_LOCATION, READ_CONTACTS)) {
        // Already have permission, do the thing
        // ...
    } else {
        // Do not have permissions, request them now
        EasyPermissions.requestPermissions(
            host = this,
            rationale = getString(R.string.permission_location_and_contacts_rationale_message),
            requestCode = REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION,
            perms = ACCESS_FINE_LOCATION, READ_CONTACTS
        )
    }
}

Or for finer control over the rationale dialog, use a PermissionRequest:

val request = PermissionRequest.Builder(spyActivity)
    .code(REQUEST_CODE)
    .perms(REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION)
    .theme(R.style.my_fancy_style)
    .rationale(R.string.camera_and_location_rationale)
    .positiveButtonText(R.string.rationale_ask_ok)
    .negativeButtonText(R.string.rationale_ask_cancel)
    .build()
EasyPermissions.requestPermissions(spyActivity, request)

Optionally, for a finer control, you can have your Activity / Fragment implement the PermissionCallbacks interface.

class MainActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks {

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        // EasyPermissions handles the request result.
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    }

    override fun onPermissionsGranted(requestCode: Int, perms: List<String>) {
        // Some permissions have been granted
        // ...
    }

    override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
        // Some permissions have been denied
        // ...
    }
}

Required Permissions

In some cases your app will not function properly without certain permissions. If the user denies these permissions with the "Never Ask Again" option, you will be unable to request these permissions from the user and they must be changed in app settings. You can use the method EasyPermissions.somePermissionPermanentlyDenied(...) to display a dialog to the user in this situation and direct them to the system setting screen for your app:

Note: Due to a limitation in the information provided by the Android framework permissions API, the somePermissionPermanentlyDenied method only works after the permission has been denied and your app has received the onPermissionsDenied callback. Otherwise the library cannot distinguish permanent denial from the "not yet denied" case.

override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
    Log.d(TAG, "onPermissionsDenied: $requestCode :${perms.size()}")

    // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
    // This will display a dialog directing them to enable the permission in app settings.
    if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
        SettingsDialog.Builder(this).build().show()
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == DEFAULT_SETTINGS_REQ_CODE) {
        val yes = getString(R.string.yes)
        val no = getString(R.string.no)

        // Do something after user returned from app settings screen, like showing a Toast.
        Toast.makeText(
            this,
            getString(
                R.string.returned_from_app_settings_to_activity,
                if (hasCameraPermission()) yes else no,
                if (hasLocationAndContactsPermissions()) yes else no,
                if (hasSmsPermission()) yes else no,
                if (hasStoragePermission()) yes else no
            ),
            LENGTH_LONG
        ).show()
    }
}

Interacting with the rationale dialog

Implement the EasyPermissions.RationaleCallbacks if you want to interact with the rationale dialog.

override fun onRationaleAccepted(requestCode: Int) {
    // Rationale accepted to request some permissions
    // ...
}

override fun onRationaleDenied(requestCode: Int) {
    // Rationale denied to request some permissions
    // ...
}

Rationale callbacks don't necessarily imply permission changes. To check for those, see the EasyPermissions.PermissionCallbacks.

LICENSE

	Copyright 2017 Google

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

Comments
  • base library error after implementation

    base library error after implementation

    After implementing the library, these base Androidx errors appear! What's the problem?

    Execution failed for task ':app:dataBindingMergeDependencyArtifactsDebug'.
    > Could not resolve all files for configuration ':app:debugCompileClasspath'.
       > Could not find fragment-1.2.5.aar (androidx.fragment:fragment:1.2.5).
         Searched in the following locations:
             https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.2.5/fragment-1.2.5.aar
       > Could not find fragment-1.2.5.aar (androidx.fragment:fragment:1.2.5).
         Searched in the following locations:
             https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.2.5/fragment-1.2.5.aar
       > Could not find fragment-1.2.5.aar (androidx.fragment:fragment:1.2.5).
         Searched in the following locations:
             https://dl.google.com/dl/android/maven2/androidx/fragment/fragment/1.2.5/fragment-1.2.5.aar
    
    question 
    opened by behnawwm 2
  • Removing jcenter() error in build

    Removing jcenter() error in build

    Basic Information

    Device type:Android OS version: 11 EasyPermissions version: all

    Describe the problem

    With the new Android X JCenter() is now deprecated. when removing it fro gradle we get Failed to resolve: com.vmadalin:easypermissions-ktx:0.1.0

    Code and logs

    Failed to resolve: com.vmadalin:easypermissions-ktx:0.1.0 Show in Project Structure dialog Affected Modules: app

    // TODO(you): show the code that produces the problem, // and any relevant logs.

    bug 
    opened by techker 2
  • Infinite loop occurs when displaying SettingsDialog with onPermissionsDenied

    Infinite loop occurs when displaying SettingsDialog with onPermissionsDenied

    Basic Information

    Device type: Emulator (Pixel4 API 29) OS version: Android 10 EasyPermissions version: 1.0.0

    Question

    Sorry for the series of questions.

    I'm trying to create a function like "Open app settings when some permissions are permanently denied", but it doesn't work.

    In the onPermissionsDenied callback, I evaluate somePermissionPermanentlyDenied() and open the SettingsDialog from there, but the Fragment from which the call is made is being called infinitely.

    画面収録 2021-12-04 午前11 34 26

    Is it wrong to try to open the SettingsDialog at this location? If so, how do I open the SettingsDialog correctly?

    Code and logs

    I have uploaded the source code to github. https://github.com/Nunocky/EasyPermissionsStudy

        override fun onResume() {
            super.onResume()
    
            checkPermissions()
        }
    
        //  -----------------------------------------------
        //  permissions issue
        //  -----------------------------------------------
        private val requiredPermissions = listOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
        )
    
        private fun checkPermissions() {
            if (!EasyPermissions.hasPermissions(
                    requireContext(),
                    *(requiredPermissions.toTypedArray())
                )
            ) {
                // Do not have permissions, request them now
                EasyPermissions.requestPermissions(
                    host = this,
                    rationale = "permissions required",
                    requestCode = REQUEST_PERMISSIONS,
                    perms = requiredPermissions.toTypedArray()
                )
            }
        }
    
        override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
            if (EasyPermissions.somePermissionPermanentlyDenied(this, requiredPermissions)) {
                toast("somePermissionPermanentlyDenied")
    
                SettingsDialog.Builder(requireContext())
                    .build()
                    .show()
           }
        }
        
    
    opened by Nunocky 1
  • release version 0.1.0 is different from the source code

    release version 0.1.0 is different from the source code

    Basic Information

    Device type: ________ OS version: ________ EasyPermissions version: ________

    Describe the problem

    What happened? What did you expect to happen?

    Code and logs

    // TODO(you): show the code that produces the problem,
    //            and any relevant logs.
     @JvmStatic
        fun somePermissionPermanentlyDenied(
            host: Activity,
            @Size(min = 1) vararg deniedPerms: String
        ): Boolean {
            return PermissionsHelper.newInstance(host).somePermissionPermanentlyDenied(deniedPerms)
        }
    
    documentation 
    opened by NIUDEYANG123 1
  • Migrate project to Kotlin

    Migrate project to Kotlin

    logo

    Original PR

    https://github.com/googlesamples/easypermissions/pull/289

    Description

    Migrate all entire project to Kotlin 1.3.50, but keeping the integration concept. These are some of the things that have been done:

    • Migrate entire sample to kotlin
    • Make the library more modular by structure better the files / code
    • Add a project logo, make more easy to identify it
    • Add severals tools like ktlint, detekt, spotless for static analysis code adding them to Travis
    • Migrate all the tests to kotlin, for guaranty the coverage for new code
    • Simplify logic about Rationale/Settings dialogs displaying an AlertDialog
    • Eradicate LowAPiPermissions for avoid throw exception to integrator on their code

    Overview

    • Architecture:
    Screenshot 2019-09-06 at 11 49 22
    • Coverage:
    Screenshot 2019-09-06 at 17 10 43
    opened by vmadalin 1
  • Migrate to maven central

    Migrate to maven central

    Description

    Migrate library from JCenter to Maven Central. https://github.com/VMadalin/easypermissions-ktx/issues/4

    https://issues.sonatype.org/browse/OSSRH-68332 https://search.maven.org/artifact/com.vmadalin/easypermissions-ktx/1.0.0/aar

    opened by vmadalin 0
  • Cryptic error trying to instantiate TestSupportFragmentActivity

    Cryptic error trying to instantiate TestSupportFragmentActivity

    Basic Information

    Device type: GIONEE S10L OS version: Android 9 EasyPermissions version: 1.0.0

    Describe the problem

    I have got one crash report from crashlytics with the following stacktrace.

    Fatal Exception: java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{org.totschnig.myexpenses/com.vmadalin.easypermissions.components.TestSupportFragmentActivity}: java.lang.ClassNotFoundException: Didn't find class "com.vmadalin.easypermissions.components.TestSupportFragmentActivity" on path: DexPathList[[zip file "/system/framework/org.apache.http.legacy.boot.jar", zip file "/data/app/org.totschnig.myexpenses-Wq04CPgZmNJUUtoxLQjTGg==/base.apk"],nativeLibraryDirectories=[/data/app/org.totschnig.myexpenses-Wq04CPgZmNJUUtoxLQjTGg==/lib/arm64, /system/lib64]]
           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2881)
           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
           at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
           at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
           at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
           at android.os.Handler.dispatchMessage(Handler.java:106)
           at android.os.Looper.loop(Looper.java:193)
           at android.app.ActivityThread.main(ActivityThread.java:6718)
           at java.lang.reflect.Method.invoke(Method.java)
           at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
    Caused by java.lang.ClassNotFoundException: Didn't find class "com.vmadalin.easypermissions.components.TestSupportFragmentActivity" on path: DexPathList[[zip file "/system/framework/org.apache.http.legacy.boot.jar", zip file "/data/app/org.totschnig.myexpenses-Wq04CPgZmNJUUtoxLQjTGg==/base.apk"],nativeLibraryDirectories=[/data/app/org.totschnig.myexpenses-Wq04CPgZmNJUUtoxLQjTGg==/lib/arm64, /system/lib64]]
           at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
           at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
           at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
           at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:69)
           at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:45)
           at android.app.Instrumentation.newActivity(Instrumentation.java:1215)
           at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2869)
           at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
           at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
           at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
           at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
           at android.os.Handler.dispatchMessage(Handler.java:106)
           at android.os.Looper.loop(Looper.java:193)
           at android.app.ActivityThread.main(ActivityThread.java:6718)
           at java.lang.reflect.Method.invoke(Method.java)
           at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
    

    I have no idea if the user uses some tool that inspects the APK and does some weird stuff with it, or if this is a legit use of my app and the OS screws up. In any case, it might be safer to move the references to test activities out of the main AndroidManifest into the test source set.

    opened by mtotschnig 1
  • PermissionRequest.Builder.perms()      ->  the param: REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION   is mismatched.

    PermissionRequest.Builder.perms() -> the param: REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION is mismatched.

    Code and logs

    val request = PermissionRequest.Builder(spyActivity) .code(REQUEST_CODE) .perms(REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION) .theme(R.style.my_fancy_style) .rationale(R.string.camera_and_location_rationale) .positiveButtonText(R.string.rationale_ask_ok) .negativeButtonText(R.string.rationale_ask_cancel) .build() EasyPermissions.requestPermissions(spyActivity, request)

    opened by 24suixinsuoyu 0
  • onRationaleAccepted and onRationaleDenied implemented in Fragment are not called

    onRationaleAccepted and onRationaleDenied implemented in Fragment are not called

    Basic Information

    Device type: Emulator (Pixel4 API 29) OS version: Android 10 EasyPermissions version: 1.0.0

    Question

    When I was implementing the EasyPermissions feature in Fragment, I noticed that these callback functions were not being called, even though I implements EasyPermissions.RationaleCallbacks.

    画面収録 2021-12-04 午前11 28 29

    I investigated in the debugger and found the following

    Execute EasyPermissions.requestPermissions, passing MainFragment as the host.

    FFu8nI6aUAAzDh2

    PermissionRequest.Builder(host.context) is called. host.context = MainActivity

    FFu9GfmaIAAzeem

    MainActivity does not implement EasyPermissions.RationaleCallbacks, so onRationaleAccepted and onRationaleDenied will not be called.

    FFu8bGqacAMfqKR

    Is this the correct behavior? Should EasyPermissions.RationaleCallbacks be implemented only in Activity?

    Code

    The source code has been uploaded to github. https://github.com/Nunocky/EasyPermissionsStudy

    class MainFragment : Fragment(), EasyPermissions.PermissionCallbacks,
        EasyPermissions.RationaleCallbacks {
    
        private val requiredPermissions = listOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
        )
    ...
    
        override fun onResume() {
            super.onResume()
            checkPermissions()
        }
    
        private fun checkPermissions() {
            if (!EasyPermissions.hasPermissions(
                    requireContext(),
                    *(requiredPermissions.toTypedArray())
                )
            ) {
                // Do not have permissions, request them now
                EasyPermissions.requestPermissions(
                    host = this,
                    rationale = "permissions required",
                    requestCode = REQUEST_PERMISSIONS,
                    perms = requiredPermissions.toTypedArray()
                )
            }
        }
    
    ...
        // TODO : not called?
        override fun onRationaleAccepted(requestCode: Int) {
            toast("onRationaleAccepted")
        }
    
        // TODO : not called?
        override fun onRationaleDenied(requestCode: Int) {
            toast("onRationaleDenied")
        }
    }
    opened by Nunocky 2
  • Added A Flow to Use Standard or Custom Rationale

    Added A Flow to Use Standard or Custom Rationale

    Summary This refactor allows users to use Easy Permissions Rationale (standard rationale) or create a custom rationale to be used in Easy Permissions flow, according to Android Guidelines to ask for Android Permissions. Standard Rationale: It's a very basic AlertDialog (already implemented on the library). Custom Rationale: It's a Lambda function where you can place a custom dialog of your preference.

    Motivation This new flow was motivated by the lack of use of custom rational dialogs with the Easy Permission library. The library is powerful and helps a lot since you don't need to handle asking for Android permission on your own, but it lacks a way to use custom Rational Dialogs created following app UI guidelines. So, I’ve proposed a method that allows Easy Permissions users to decide which path to take: Standard Rationale or Custom Rationale.

    opened by sacastrillon 0
Releases(v1.0.0)
Owner
Madalin Valceleanu
Software Developer @discovery Enthusiast and passionate about clean code, modular architecture, design patterns and automation. 🇷🇴 - 🇪🇸 - 🇬🇧
Madalin Valceleanu
Kotlin-basic-calculator - Basic calculator to understand syntax and the methods of Kotlin

KotlinBasicCalculator I always love to create a calculater for understand the sy

Onur Serbes 1 Mar 8, 2022
An small android app based on banking logic, usilng SQLITE as database, material design, navigation drawer implemented

Android Banking App Project - Using Sqlite The Banking app using java in android studio and sqlite for crud. Packages Used Material Design Contributin

Md-Mahin-Rahman 4 Dec 6, 2022
Oratio Library for Android Studio helps you simplify your Android TTS codes

Oratio Oratio is a library for Android Studio. This library is useful to a number of developers who are currently making apps using android TTS(Text-T

Jacob Lim 1 Oct 28, 2021
A Android app Permissions with Kotlin Flow APIs

Know about real-time state of a Android app Permissions with Kotlin Flow APIs. Made with ❤️ for Android Developers.

Shreyas Patil 281 Dec 30, 2022
This little project provides Kotlin bindings for the popular tree-sitter library

kotlintree This little project provides Kotlin bindings for the popular tree-sitter library. Currently it only supports the Kotlin JVM target, but Kot

Christian Banse 23 Nov 28, 2022
An android app built using Kotlin that consumes TMDB API to display current trending, upcoming and popular movies 🍿 .

Flick An android app built using Kotlin that consumes TMDB API to display current trending, upcoming and popular movies ?? .It has been built followin

Kagiri Charles 8 Nov 29, 2022
Popular Movies showcase android app

Pop Flix PopFlix is a gorgeous client application for TMDb on Android, built using Kotlin. Architecture and Tech-stack Built on MVVM architecture patt

Vishvendra Singh 5 Jun 7, 2022
The Android application is a list of the most popular TV series written in Java using the mvvm pattern.

Tv show application The Android application is a list of the most popular TV series written in Java using the mvvm pattern. This project was written f

rutikeyone 0 Dec 26, 2021
Movie streaming is an android app that show us last,newest,top imdb and popular movies.

Movies-Streaming-Android-App An Android application which shows Popular, New Movies, Top Rated movies and all the details of any movie like- Cast, Rev

mojtaba joshaghani 33 Aug 25, 2022
A minimalist clone of the popular Social Media Platform "Instagram"

InstaLocal A minimalist clone of the popular Social Media Platform "Instagram" powered by Firebase and written in Kotlin. The app allows users to sign

Raktim Bhuyan 1 Nov 7, 2021
The App Loads list of popular movies from a mock API and shows in a recyclerView

popular-movies-app About The App Loads list of popular movies from a mock API and shows in a recyclerView. Any item can be clicked to open the Movie D

Mayank Agarwal 0 Oct 30, 2021
A rewrite of the popular project GitUp that works in Linux, Mac, and Windows.

GitDown This is a rewrite from the ground up of the popular GitUp library available on Mac. It is built using Kotlin and Compose Desktop from Jetbrain

Cody Mikol 20 Dec 16, 2022
Program, created to make popular adb and fastboot commands easier to use

Android Tool What is it? Android Tool is a powerful and beautiful program, created to make popular adb and fastboot commands easier to use. A dark the

Rodion 245 Dec 29, 2022
Movie Info - MovieInfo app that recieves popular movies and allow the user to search for specific movie through the restapi

Movie_Info MovieInfo app that recieves popular movies and allow the user to sear

inderjeet yadav 3 Jun 8, 2022
Keep track of popular & top rated movies and see movie details

Movies Keep track of popular & top rated movies and see movie details Features Keep track of popular & top rated movies See movie details Libraries Je

Amr Saraya 1 May 1, 2022
This application uses Google Play Services Vision library to scan barcodes. It uses Google's on device ML kit to scan for barcodes.

Barcode-Scanner This application showcases use of Google Play Services Vision library It uses Google's on device machine learning kit to scan for barc

Soumik 2 Apr 28, 2022
Google one tap sign in - Flutter Google One Tap Sign In (Android)

Google One Tap Sign In Google One Tap Sign In (Android) A Flutter Plugin for Google One Tap Sign In Getting Started To access Google Sign-In, you'll n

null 6 Nov 23, 2022
xCloud player for Google Chromecast with Google TV

XCTV Player An awesome Microsoft xCloud player for Google Chromecast with Google

Keith Baker 41 Dec 27, 2022
Xctvplayer - xCloud player for Google Chromecast with Google TV

XCTV Player An awesome Microsoft xCloud player for Google Chromecast with Google

Keith Baker 41 Dec 27, 2022