Category: Android Development

  • Spoofing Battery and Forcing Sleep (Android Development)

    I wanted to do something with my Android phone using ADB commands, while keeping it plugged in to run the said commands. However, the thing I wanted to test was related to the idle (dozing or asleep) mode of Android devices. The workaround was to spoof the battery state to bypass Android’s charging restrictions.

    Spoofing Battery Charging Status and Forcing Android Device to Sleep (Idle Mode)

    • Turn off the device screen using the physical power button.
    • Trick Android into thinking the device is running on battery power: adb shell dumpsys battery unplug
    • Force the device to step into deep Doze mode: adb shell dumpsys deviceidle force-idle
    • Verify the device is successfully spoofed and asleep: adb shell dumpsys deviceidle get deep (This should return IDLE).

    Can also do:
    adb shell dumpsys battery unplug && adb shell dumpsys deviceidle force-idle && adb shell dumpsys deviceidle get deep

    Device Cleanup for Spoofed Battery and Forced Sleep (Crucial!)

    1. Once testing is done, you must tell the OS to start reading the actual battery state again, otherwise it won’t charge properly: adb shell dumpsys battery reset
    2. Bring the device out of idle: adb shell dumpsys deviceidle unforce

    Can also do:
    adb shell dumpsys battery reset && adb shell dumpsys deviceidle unforce && adb shell dumpsys deviceidle get deep

  • Shorter ADB commands for Android Studio Emulator Device targeting

    To target my specific emulator devices on Android Studio with my ADB (Android Debug Bridge) commands, I usually need to type long ones, like in the commands below where i wanna list the contents of a directory:

    adb -s emulator-5554 ls /sdcard/Android/sub/directory/
    adb -s emulator-5556 ls /sdcard/Android/sub/directory/

    When I am working on a new feature or a bug-fix ticket, I try to avoid typing long commands and args over and over again, especially args that are as error-prone as emulator-5554.

    How to shorten the ADB commands that target emulators (Mac OS):

    Open your Zsh config file with an editor. Mine by habit is Vim. Like so:
    vim ~/.zshrc

    Use your arrow keys to go to the very bottom of the file and copy-paste below exact code:

    adbs() {
      port=$1
      shift
      adb -s "emulator-55$port" "$@"
    }


    What it does is creates a new command adbs. I think the addition of s at the end is appropriate because the flag we are trying to work with is the -s flag which stands for “Serial”, for identifying specific emulators.

    The way the new command works is,

    • the $1 captures the first argument you type after adbs and saves it — for example, the 54 in adbs 54.
    • The shift bit removes the first argument from the front so that the actual command to be run will NOT be adb -s "emulator-5554" 54 logcat (wrong!)
    • Finally, the last line adb -s "emulator-55$port" "$@" runs the correct command, inserting the first argument (54 in our example) where $port is, followed by "$@" which grabs the rest of the command after the 54

    Save the changes to the config file and then exit the editor. In Vim I press Esc key, then type :wq! and hit Enter. IIRC my partner questions something about my :wq! usage, but what he points out, I can’t remember.

    After saving the config file, run the following command to make the new shortcut work immediately:

    source ~/.zshrc

    Usage Example:

    Instead of typing the long version:
    adb -s emulator-5554 ls /sdcard/Android/sub/directory/

    We can just type:
    adb -s 54 ls /sdcard/Android/sub/directory/

    Extra: to know what serial numbers of running devices you got, run below command

    adb devices

    Random thoughts: I am grateful to my very first team lead as a software developer (Peng) because he’s the one who inspired me to become comfortable with command line interfaces. Remembering how much I grew as a dev in that team due to high-traffic production databases that we were managing and debugging in realtime. Oh, the source command on the other hand, was something I learned from my current team lead, Maru. I like how much I am growing in this current team, not only as a dev, but also as a person.

    Credit goes to Google Gemini for the above adbs custom command. I had no idea how to write that until today. hehe

  • A random dev brain dump

    I don’t need a reason to randomly post a brain dump on my blog, no?

    • If an Android dev wishes to implement a precise “notification ID” that they want to be able to precisely cancel later (imagine that it plays an annoying alarm tone!) they can go about it either by
      • Using a unique ID tied to that notification’s base entity (e.g., an app’s incoming message) so that NOTIFS WILL STACK, or
      • Using a unique, dedicated ID that is defined as a constant and is defined without ties to the notification’s base entity so that ONLY THE LAST NOTIF WILL BE SHOWN
    • I gotta update my site’s, something..
    • If an Android dev’s soft keyboard covers a text input field when it’s displayed, particularly in Android 15 devices, they may wanna try to add an imePadding modifier to the root element (e.g., Box, Column) because this modifier will take care of spacings whenever the keyboard appears, avoiding issues such as covering the text input field.
  • Fix “Error loading build artifacts” error (Android Studio after upgrade)

    Problem:

    After upgrading Android Studio from “Ladybug” to “Otter” I started getting the following error message when I run my app on my emulated test device:

     "Error loading build artifacts from: .../myProjectRootDir/app/build/intermediates/apk_ide_redirect_file/productionDebug/createProductionDebugApkListingFileRedirect/redirect.txt"


    Solution:

    First, I ran the AGP Upgrade Assistant, which updated 2 files:

    • .../myProjectRootDir/gradle/wrapper/gradle-wrapper.properties
      Distribution URL has been upgraded from distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip to distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
    • .../myProjectRootDir/gradle/libs.versions.toml
      AGP version updated from agp = '8.8.1' to agp = '8.13.2'

    And then, applied a “Run Configuration” Fix:

    1. Click the Edit Configurations dropdown (next to the “Play” button).
    2. Select your app configuration on the left.
    3. Scroll to the “Before Launch” section at the bottom.
    4. Remove “Gradle-aware Make” (select it and click the minus – button).
    5. Re-add it: Click +, select Gradle-aware Make, and leave the task field empty. Click OK.
    6. Click Apply -> OK.
    7. Run the app.

    Additional things to try if above suggestions weren’t sufficient to fix the issue:

    • Invalidate caches
    • Clean the build before re-running
    • Sync Gradle files
  • Android emulator shell list fails: Permission Denied in app directory

    Today I learned I should include the app package name after shell in my ADB command, if for example I want to list files under the “subdirectory/media” directory of an app, like so:

    adb -s emulator-5556 shell run-as com.domain.my.app ls -al /data/user/0/com.domain.my.app/subdirectory/media

    First try’s command below failed with a Permission Denied error message:

    adb -s emulator-5556 shell ls -al /data/user/0/com.domain.my.app/subdirectory/media

    Makes sense, right? Because part of Android OS’ security is limiting access to app directories. But if you “run-as” the package owner app itself, it should grant you access.

  • Android: Broadcast sent, receiver registered, but receiver doesn’t receive broadcast

    I should blog more often about things I learn about.

    In a native Android app written in Kotlin, I have a LifecycleService class with below code to send a broadcast:

        val eventIntent = Intent(CUSTOM_EVENT_ACTION_NAME).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
        }
        applicationContext.sendBroadcast(eventIntent)
        Log.d("AppTag", "Broadcast sent!")



    Based on the logcat, the broadcast is being sent.

    However, below BroadcastReceiver in an Activity doesn’t receive the broadcast, even while it is visible:

    
    private val customEventReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            Log.d("AppTag", "Custom event receiver triggered")
            if (intent?.action == GlobalConstants.CUSTOM_EVENT_ACTION_NAME) {
                Log.d("AppTag", "Confirmed action match, proceeding with task.")
                performSpecificTask()
            }
        }
    }

    The above BroadcastReceiver is registered from the Activity’s onCreate function, like so:

    val filter = IntentFilter(GlobalConstants.CUSTOM_EVENT_ACTION_NAME)
    ContextCompat.registerReceiver(
        this,
        customEventReceiver,
        filter,
        ContextCompat.RECEIVER_NOT_EXPORTED 
    )
    
    

    Attempt #1: LocalBroadcastManager – it worked, but…

    After browsing the web for potential solutions I came across LocalBroadcastManager. The broadcast was successfully received by the receiver after I changed the sendBroadcast’s applicationContext and the registerBroadcast’s ContextCompat into LocalBroadcastManager.getInstance(Context).

    That is, LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(eventIntent) and LocalBroadcastManager.getInstance(this).registerReceiver(customEventReceiver, filter).

    The problem with this approach is that LocalBroadcastManager has been deprecated:

    Important changes since 1.0.0

    androidx.localbroadcastmanager has been fully deprecated. There will be no further releases of this library. Developers should replace usages of LocalBroadcastManager with other implementations of the observable pattern. Depending on the use case, suitable options may be LiveData or reactive streams.

    https://developer.android.com/jetpack/androidx/releases/localbroadcastmanager

    Ideally, the solution should not be a deprecated one.

    As to why LocalBroadcastManager made my receiver work, I think it’s because it “bridged” 2 components that weren’t really supposed to be able to communicate. Because from the Android docs:

    LocalBroadcastManager is an application-wide event bus and embraces layer violations in your app: any component may listen events from any other.

    Why it works, and why it’s been deprecated, apparently have the same explanation. 😅

    Attempt #2: Use applicationContext on the sender and receiver — Nope!

    I reverted my code to the original, and just made sure they all used applicationContext, matching the context used in the service’s broadcast sender applicationContext.sendBroadcast(eventIntent).

    So that my registration became:

    val filter = IntentFilter(GlobalConstants.CUSTOM_EVENT_ACTION_NAME)
    ContextCompat.registerReceiver(
        applicationContext,
        customEventReceiver,
        filter,
        ContextCompat.RECEIVER_NOT_EXPORTED 
    )
    
    
    And the code to unregister became:
    
    applicationContext.unregisterReceiver(customEventReceiver)

    Hmm… Nope! that didn’t let my receiver get the broadcast.

    Attempt #3: Make it an explicit broadcast — YES, IT WORKS!

    I reverted my code again to try another approach, which is to make the broadcast an explicit one.

    So, in my service, I simply set the package name as shown in code below:

    val eventIntent = Intent(CUSTOM_EVENT_ACTION_NAME).apply {
        setPackage(applicationContext.packageName)
        //...

    And with this 1 line changed in code, my BroadcastReceiver now receives the broadcast! Plus, there’s no deprecated class used here so we prevented 1 new technical debt. :3

    Yeah, I think I’ll stick to the 3rd attempt’s approach!

    Why does this approach work, though? It’s because starting from Android API level 26, there’s been restriction on implicit broadcasts.

    As part of the Android 8.0 (API level 26) background execution limits, apps that target the API level 26 or higher can’t register broadcast receivers for implicit broadcasts in their manifest unless the broadcast is sent specifically to them. However, several broadcasts are exempted from these limitations. 

    https://developer.android.com/develop/background-work/background-tasks/broadcasts/broadcast-exceptions

    My original code’s broadcast Intent was an implicit one, but specifying a package name converted it into an explicit one, consequently lifting the limitation applied to implicit broadcasts!

  • Run command automatically after updating a file on Mac

    I get a kick out of automating things! Especially those seemingly easy extra steps that we do numerous times a day, everyday.

    There’s this configuration file that I frequently have to adb push into my test Android devices. And the cycle of editing the file, saving it, and then going to the terminal to run the push command (sometimes having to press the up ↑ key a few times) , is repeated as many times as needed.

    Last week, while rapidly becoming exhausted from the extreme summer heat, I was becoming frustrated at the bug I was trying to fix. Having to repeat the edit-save-push cycle on the file a countless number of times, along with the discomfort from sweaty armpits, wasn’t helpful in fixing the bug.

    I wondered, how do I automatically push this configuration file when unix detects a change in the file after editing and saving?

    What I did was rely on “entr”.

    I first installed it on my mac like so: brew install entr

    And then ran below command in my terminal:

    ls /Users/catz/Documents/MyConfigFile.json | | entr adb -e push /Users/catz/Documents/MyConfigFile.json /sdcard/Android/data/com.example.app/files/config/MyConfigFile.json

    /Users/catz/Documents/MyConfigFile.json is the configuration file I frequently edit and push.

    /sdcard/Android/data/com.example.app/files/config/MyConfigFile.json is the expected location of the file in my test Android device. The app reads the configuration from this location.

    Now, with this setup, I simply have to hit Save after editing the configuration file, and I just watch it automatically get pushed to my test Android device. 😎 My younger self probably would have tried to code her own script for automation, but my old, cranky present self knows better than waste time like that. Heh.

    Automating little things, such as having to push a changed file each and every time it’s edited, can have a big impact in one’s workflow. Streamlining mine this way allows me to use my little energy on what really matters.

    So, how did the bug squashing go?

    A quick background on the bug: StateFlow.update was not updating a value inside a list of data class model. I thought that a deep copy was what I needed, but it turned out that the data class contained an override function of equals and it was lacking a necessary logic to return the correct Boolean value. StateFlow.compareAndSet, which is called by StateFlow.update behind the scenes, does not proceed with updating the value, because the old and new value are seen as “equal” due to the overridden equals function not returning the correct result.

    I didn’t notice the faulty equals logic until I regained my calm focus on that hot workday — all thanks to a quick cool shower, and of course, the kick I got from setting up my command to automatically run after updating the configuration file~

    And yes, remember to try to feel comfortable as you do problem solving tasks! I think that feeling comfortable helps us keep a clear head, which consequently helps us solve problems more effectively.

  • Make a RecyclerView expand its height when new item is added //Android Dev Diary

    It seems that simply using android:layout_height="wrap_content" on your RecyclerView won’t make its height automatically expand when new items are added in it.

    (more…)
  • How to start using view binding //Android Kotlin Dev Diary

    First, open your app/build.gradle file

    Inside the android{} block of app/build.gradle, add the following:

    buildFeatures { viewBinding = true }

    Sync project with gradle files.

    In your activity or fragment, define the following class-wide property:

    private lateinit var binding: FragmentorActivityNameBinding

    Replace the FragmentorActivityNameBinding above with the appropriate binding class name. Tip: The binding class name is based on the XML file’s name. For example, if your XML file name is activity_main.xml, the binding class name should be ActivityMainBinding. Or, if your file name is fragment_main.xml, the binding class name should be FragmentMainBinding.

    In your activity, the onCreate function should look like this:

    override fun onCreate(savedInstanceState: Bundle?) {
    
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    Otherwise, if using view binding in a fragment, your onCreateView should look like this:

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
    
        binding = FragmentMainBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = mainViewModel
        return binding.root
    }
  • Disable animations programmatically during instrumented tests // Android Dev Diary

    When running an instrumented Android test, or “androidTest”, and you encounter the following error:

    androidx.test.espresso.PerformException: Error performing 'scroll to' on view 'Animations or transitions are enabled on the target device.

    It means that you need to disable animations for it to successfully run the test.

    But, it can take a lot of clicks on the emulator/device screen to get to the developer settings to turn off animations!

    So, for a quicker way to disable animations in the Android device during development, just enter the following commands in your Android Studio terminal:

    adb shell settings put global window_animation_scale 0.0
    adb shell settings put global transition_animation_scale 0.0
    adb shell settings put global animator_duration_scale 0.0