Skip to content

Add notification command for turning the hotspot on/off #5322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

moemoeq
Copy link

@moemoeq moemoeq commented May 19, 2025

Summary

Added hotspot control functionality through Notification Commands in Home Assistant Android app. This feature allows users to remotely toggle the hotspot function from Home Assistant.

  • Uses TetheringManager to control hotspot
    • Supported only on Android 11 (API 30) and above
  • Enables hotspot activation/deactivation through notifications

Example to Trigger:
Turn on hotspot:

automation:
  - alias: Turn on hotspot
    trigger:
      ...
    action:
      - action: notify.mobile_app_<your_device_id_here>
        data:
          message: "command_hotspot"
          data:
            command: "turn_on"

Turn off hotspot:

automation:
  - alias: Turn on hotspot
    trigger:
      ...
    action:
      - action: notify.mobile_app_<your_device_id_here>
        data:
          message: "command_hotspot"
          data:
            command: "turn_off"

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Screenshots

Link to pull request in documentation repositories

User Documentation: home-assistant/companion.home-assistant#
I'll update the documentation PR after getting feedback on this one.

Any other notes

Related Issue : #2654
This feature only works on Android 11 (API 30) and above. The hotspot control functionality is implemented using TetheringManager, and reflection is used to access system APIs.

To wider device compatibility, I think also implement a method using ConnectivityManager, although I don't have any device to test this. (> android 11)
However, I've included code with scalability in mind, considering this implementation (even if it seems unnecessary now).

I've verified it on two of my Samsung devices G977N(API 31), G906N(API 34).

I'm a developer with very little experience in Android.
I'm still figures out linting issues yet, and I'm wondering if my implementation aligns with this project's direction. I'd also appreciate it if you could explain what I need to refine.
This implementation was essential for me to achieve a seamless connection between my car and home automations. thank you.

Copy link

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @moemoeq

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

- Introduced HotspotHelper class for managing Wi-Fi hotspot operations on Android 11 (API 30) using TetheringManager
- Integrated hotspot commands into MessagingManager, allowing for enabling and disabling the hotspot via notifications.
- Added a new string resource for missing write settings permission notification.
@moemoeq moemoeq force-pushed the issue-2654-adding-hotspot branch from e0b4d3f to cd88fea Compare May 19, 2025 03:10
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ktlint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

}

// API 30+ needed for TetheringManager
@RequiresApi(Build.VERSION_CODES.R)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could probably make the whole class RequiresAPI 30 no?

return true
}

// TODO: implement using ConnectivityManager as fallback
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I would keep this feature for API 30 with this API and create an issue for previous API (you can link the issue in the TODO). With this you can track how many people would be interested into having this feature in old version, before jumping into using the old API.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a great idea. If that's the case, I think it's possible make the whole class mark RequiresAPI 30 in this PR

Comment on lines +121 to +126
val startMethod = tetheringManager.javaClass.getMethod(
"startTethering",
tetheringRequestClass,
Executor::class.java,
callbackClass
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not use reflection here.

            val tetheringManager = context.getSystemService<TetheringManager>()
            if (tetheringManager == null) {
                Timber.e("TetheringManager is null")
                return false
            }
            tetheringManager.startTethering(TetheringManager.TetheringRequest.Builder(TetheringManager.TETHERING_WIFI).build(),
                mainExecutor, object : TetheringManager.StartTetheringCallback {})

That's how you get the TetheringManger you need to adjust the class to not use reflection.

https://developer.android.com/reference/android/net/TetheringManager the API seems to have been added very recently API 36.

Copy link
Author

@moemoeq moemoeq May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look! I opened this PR mainly because I wanted to get feedback on this specific point.

During development, I just knows official TetheringManager public API (without reflection) is now available starting from API 36 (Android 16), which is still in beta. Because of that, I implemented the current version (using reflection) to support API 30 and above, since that’s where most devices are in use today.(Anyway, it's wider than API 36, right?)

I’ve noticed that similar approaches are used in other apps and some posts, so I followed that pattern. but, my understanding of the long-term implications is still somewhat limited.

One idea I had was to branch the logic by API level — using the public API for API 36+ and falling back to reflection for earlier versions. I’d love to hear your thoughts on whether that would be a more appropriate or acceptable approach.

Also, more generally: is using reflection to access hidden APIs something this project prefers to avoid? Or is it conditionally acceptable depending on use cases like this? If there are any specific guidelines I should follow, I’d really appreciate being pointed in the right direction.

Thanks again for your input!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity did you try to use the official API on lower version? It might work since it works with reflection. If you look at the impl of TetheringManager they doesn't specify any API version so I'm curious.

I get why other app used reflection to access this API if it was not publicly available. But since it has been released and our compile version is 36 it might work. Again not sure at all it needs testing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to use reflection, I'm not a fan at all. It should be our last resort to fix something. I would say that for implementing something new I wouldn't be ok.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Lucky for me, I’ve got devices on API 31 and 34 in my hand and Android Studio’s already open 😄 I’ll run some tests using with official API on lower versions and let you know how it goes.

And true, I’m with you reflection and private APIs are exactly not ideal. I needed the functionality and, honestly, didn’t find a solid guideline on whether this was “acceptable,” so I went with a solution that seemed to work across more devices for now.

I’ll report back soon!

Copy link
Author

@moemoeq moemoeq May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right — I totally missed that. Thanks for pointing it out, I’ll make sure to include permalinks next time!

I tested it using the official TetheringManager API, and interestingly, it worked fine on both of my devices (API 31 and 34).

That was unexpected, but definitely a nice surprise. maybe we can drop the reflection.
Here’s the snippet I used for testing, just in case:

    private fun enableHotspotWithTetheringManager(): Boolean {
        try {
            val tetheringManager = context.getSystemService<TetheringManager>()
            if (tetheringManager == null) {
                Timber.e("TetheringManager is null")
                return false
            }

            tetheringManager.startTethering(TetheringManager.TetheringRequest.Builder(TetheringManager.TETHERING_WIFI).build(),
                mainExecutor,
                object : TetheringManager.StartTetheringCallback {
                    override fun onTetheringStarted() {
                        Timber.d("Callback: Tethering started successfully")
                    }

                    override fun onTetheringFailed(result: Int) {
                        Timber.e("Callback: Tethering failed to start Result: $result")
                    }
                }
            )

Copy link
Author

@moemoeq moemoeq May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure but, I assumed it’s been functional since API 30(since reflection access worked back then). but, I’m not very experienced with android devs, curious to hear your take!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API was there before but not public. They've made it public now without specifying what is the minimal version from an annotation. If you check the source here you can see the Copyright from 2019.

Most of the functions are marked @SystemApi

Indicates an API is exposed for use by bundled system applications.
These APIs are not guaranteed to remain consistent release-to-release,
and are not for use by apps linking against the Android SDK.

and the rest with @SuppressLint("UnflaggedApi").

Copy link
Author

@moemoeq moemoeq May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the clarification, After reading your comment and digging into the AOSP source myself
android-29
android-30

From these, now i'm clear TetheringManager has existed since API 30 (just marked as @SystemApi back then). With that in mind, I’ll drop the reflection and implement the hotspot functionality directly using the TetheringManager API for API 30+

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some sad news to share. During our attempt to switch to the official public TetheringManager API, StopTetheringCallback results in a "class not found" error. even StartTetheringCallback works properly from API 30(on my API 34 device)

so, i think we have two options:

  1. Implement only the disable hotspot functionality using reflection while keeping the enable functionality with the direct API call
  2. Implement both enable and disable functionality consistently with reflection

Which approach would be more maintainable and reliable in the long term? I'm concerned about potential inconsistencies with a mixed approach, but also hesitant to revert working direct API implementations unnecessarily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants