<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: cat dog (running_in_the_storm)</title>
    <description>The latest articles on Forem by cat dog (running_in_the_storm) (@cat_dogrunning_in_the_s).</description>
    <link>https://forem.com/cat_dogrunning_in_the_s</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3468568%2Fc299ba2d-971e-4cb9-9a74-b34aeada7196.png</url>
      <title>Forem: cat dog (running_in_the_storm)</title>
      <link>https://forem.com/cat_dogrunning_in_the_s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cat_dogrunning_in_the_s"/>
    <language>en</language>
    <item>
      <title>Efficient Android Screen Recording Using MediaRecorder + MediaProjection</title>
      <dc:creator>cat dog (running_in_the_storm)</dc:creator>
      <pubDate>Wed, 12 Nov 2025 14:56:24 +0000</pubDate>
      <link>https://forem.com/cat_dogrunning_in_the_s/efficient-android-screen-recording-using-mediarecorder-mediaprojection-27ib</link>
      <guid>https://forem.com/cat_dogrunning_in_the_s/efficient-android-screen-recording-using-mediarecorder-mediaprojection-27ib</guid>
      <description>&lt;h1&gt;
  
  
  Efficient Android Screen Recording Using MediaRecorder + MediaProjection
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;On the Android platform, there are two primary approaches to implement screen recording: the &lt;strong&gt;MediaCodec + MediaMuxer&lt;/strong&gt; combination and &lt;strong&gt;MediaRecorder&lt;/strong&gt;. The former offers high flexibility and customization but is relatively complex, requiring manual handling of video/audio encoding and muxing processes.&lt;/p&gt;

&lt;p&gt;In contrast, &lt;strong&gt;MediaRecorder&lt;/strong&gt; is a high-level API that encapsulates the underlying audio/video recording and encoding workflows, significantly reducing development effort while maintaining excellent performance and compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MediaProjection&lt;/strong&gt;, introduced in Android 5.0 (API 21), is a system-level service that resolves the core challenge of screen recording—how to securely and efficiently capture screen content. It provides a standardized, app-oriented interface for screen capture.&lt;/p&gt;

&lt;p&gt;This article explains how to efficiently implement Android screen recording using &lt;strong&gt;MediaRecorder + MediaProjection&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core of MediaRecorder: Lifecycle State Machine
&lt;/h2&gt;

&lt;p&gt;To use &lt;code&gt;MediaRecorder&lt;/code&gt; effectively, you must understand its working principles and master its &lt;strong&gt;lifecycle states&lt;/strong&gt;. The state transitions are strict—any incorrect operation may cause exceptions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initial State&lt;/strong&gt;: The default state when &lt;code&gt;MediaRecorder&lt;/code&gt; is first created. You can return to this state via &lt;code&gt;new MediaRecorder()&lt;/code&gt; or &lt;code&gt;reset()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configuring State&lt;/strong&gt;: In this state, you configure recording parameters using various &lt;code&gt;set...&lt;/code&gt; methods, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setAudioSource(MediaRecorder.AudioSource.MIC)&lt;/code&gt;: Set audio source.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setVideoSource(MediaRecorder.VideoSource.SURFACE)&lt;/code&gt;: Set video source to a &lt;code&gt;Surface&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)&lt;/code&gt;: Set output container format.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setVideoEncoder(MediaRecorder.VideoEncoder.H264)&lt;/code&gt;: Set video encoder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setOutputFile(filePath)&lt;/code&gt;: Specify the output file path.
After configuration, call &lt;code&gt;prepare()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prepared State&lt;/strong&gt;: Entered after calling &lt;code&gt;prepare()&lt;/code&gt;. &lt;code&gt;MediaRecorder&lt;/code&gt; performs internal checks and resource allocation to ensure readiness for recording.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recording State&lt;/strong&gt;: Entered after calling &lt;code&gt;start()&lt;/code&gt;. &lt;code&gt;MediaRecorder&lt;/code&gt; begins capturing, encoding, and writing audio/video data to the file. This is the core phase of recording.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stopped State&lt;/strong&gt;: Entered after calling &lt;code&gt;stop()&lt;/code&gt;. Recording halts, file finalization completes, and some resources are released. &lt;strong&gt;Note&lt;/strong&gt;: You cannot resume recording into the same file once stopped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error State&lt;/strong&gt;: Entered if an error occurs at any stage, triggering a &lt;code&gt;RuntimeException&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;reset()&lt;/code&gt;: Returns &lt;code&gt;MediaRecorder&lt;/code&gt; to the &lt;strong&gt;Initial&lt;/strong&gt; state for reconfiguration and new recording.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release()&lt;/code&gt;: Fully releases all resources held by &lt;code&gt;MediaRecorder&lt;/code&gt;. The object becomes unusable afterward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design philosophy of &lt;code&gt;MediaRecorder&lt;/code&gt; prioritizes &lt;strong&gt;ease of use&lt;/strong&gt;, abstracting complex audio/video encoding and muxing behind a simple interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Features of MediaProjection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User Authorization Mechanism&lt;/strong&gt;: Requires explicit user consent via a system dialog to ensure privacy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Display Support&lt;/strong&gt;: Creates a &lt;code&gt;VirtualDisplay&lt;/code&gt; to redirect screen content to a &lt;code&gt;Surface&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System-Level Integration&lt;/strong&gt;: Interacts directly with the display system without requiring root access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compared to traditional root-based or ADB-based solutions, &lt;strong&gt;MediaProjection&lt;/strong&gt; provides a &lt;strong&gt;legal, standardized, app-friendly interface&lt;/strong&gt;, enabling screen recording apps to be published on official app stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Your Screen Recording App
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Permission Requests and User Authorization
&lt;/h3&gt;

&lt;p&gt;Screen recording is a long-running task. To prevent the app from being killed by the system, run the recording task in a &lt;strong&gt;foreground service&lt;/strong&gt; and display recording status in the notification bar.&lt;/p&gt;

&lt;p&gt;We create a foreground service &lt;code&gt;RecordingService&lt;/code&gt; for screen recording. First, declare necessary permissions like &lt;code&gt;RECORD_AUDIO&lt;/code&gt; and &lt;code&gt;FOREGROUND_SERVICE&lt;/code&gt; in &lt;code&gt;AndroidManifest.xml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;manifest&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;
        &lt;span class="err"&gt;&amp;lt;service&lt;/span&gt;
            &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".services.RecordingService"&lt;/span&gt;
            &lt;span class="na"&gt;android:foregroundServiceType=&lt;/span&gt;&lt;span class="s"&gt;"mediaProjection"&lt;/span&gt;
            &lt;span class="na"&gt;android:enabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
            &lt;span class="na"&gt;android:exported=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/service&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/application&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.FOREGROUND_SERVICE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a Compose function, use &lt;code&gt;MediaProjectionManager&lt;/code&gt; and &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt; to request screen capture permission. This step triggers a system dialog asking for user authorization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Define ActivityResultLauncher&lt;/span&gt;
&lt;span class="c1"&gt;//    It takes an Intent as input and returns an ActivityResult&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;screenCaptureLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLauncherForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartActivityForResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resultCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// User granted permission, start recording service&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startRecordingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Function to launch screen capture request&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;startScreenCaptureRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="c1"&gt;// Get MediaProjectionManager&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;projectionManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSystemService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MEDIA_PROJECTION_SERVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;MediaProjectionManager&lt;/span&gt;

        &lt;span class="c1"&gt;// Create screen capture intent&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;projectionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScreenCaptureIntent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// Launch intent using Compose launcher&lt;/span&gt;
        &lt;span class="n"&gt;screenCaptureLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="c1"&gt;// Request screen recording&lt;/span&gt;
&lt;span class="nf"&gt;startScreenCaptureRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;startRecordingService&lt;/code&gt; in the ViewModel starts a foreground recording service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startRecordingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;serviceIntent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RecordingService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ACTION_START"&lt;/span&gt;
        &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resultCode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;ContextCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startForegroundService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serviceIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BIND_AUTO_CREATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Bind to Service&lt;/span&gt;
    &lt;span class="n"&gt;binder_flag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core of Screen Recording: Foreground Service &lt;code&gt;RecordingService&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The core logic resides in &lt;code&gt;RecordingService&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
In &lt;code&gt;onStartCommand&lt;/code&gt;, we handle the recording request, create a notification, register a callback for when recording ends, and finally start the recording task. &lt;strong&gt;This order is critical&lt;/strong&gt;—otherwise, &lt;code&gt;MediaProjection&lt;/code&gt; cannot properly capture screen data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStartCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"ACTION_START"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRecording&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;START_NOT_STICKY&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;resultCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getIntExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resultCode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_CANCELED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Use the new, type-safe getParcelableExtra method&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SDK_INT&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_CODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TIRAMISU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParcelableExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nd"&gt;@Suppress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEPRECATION"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParcelableExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultCode&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Screen recording permission denied, cannot start service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LENGTH_SHORT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="nf"&gt;stopSelf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;START_NOT_STICKY&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="c1"&gt;// Ensure notification channel exists before creating notification&lt;/span&gt;
                &lt;span class="nf"&gt;createNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRecordingNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;startForeground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notificationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// 2. Register anonymous callback object (more concise!)&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;recordingCallback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;MediaProjection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="c1"&gt;// Perform cleanup when user/system revokes permission&lt;/span&gt;
                        &lt;span class="c1"&gt;// Note: We must manually unregister it. Although stop() releases resources,&lt;/span&gt;
                        &lt;span class="c1"&gt;// it's best to clean up immediately after onStop() is called.&lt;/span&gt;
                        &lt;span class="n"&gt;mediaProjection&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;unregisterCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;mediaProjection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mediaProjectionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMediaProjection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Register callback using current thread's Handler (null)&lt;/span&gt;
                &lt;span class="n"&gt;mediaProjection&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;registerCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordingCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;startRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;"ACTION_STOP"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;stopRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;START_NOT_STICKY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;startRecording()&lt;/code&gt;, we configure &lt;code&gt;MediaRecorder&lt;/code&gt;, set video/audio sources, output format, encoder, resolution, etc., and pass the screen data captured by &lt;code&gt;MediaProjection&lt;/code&gt; to &lt;code&gt;MediaRecorder&lt;/code&gt; via a &lt;code&gt;Surface&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Configure MediaRecorder&lt;/span&gt;
        &lt;span class="n"&gt;mediaRecorder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SDK_INT&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_CODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordAudioType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;setAudioSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AudioSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;setAudioEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AudioEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AAC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="nf"&gt;setVideoSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SURFACE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;setOutputFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OutputFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MPEG_4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mediaUri&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;pfd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contentResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFileDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mediaUri&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pfd&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nf"&gt;setOutputFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pfd&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fileDescriptor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;setOutputFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="c1"&gt;// Video configuration&lt;/span&gt;
                &lt;span class="nf"&gt;setVideoSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VIDEO_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;VIDEO_HEIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Example resolution&lt;/span&gt;
                &lt;span class="nf"&gt;setVideoEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;H264&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;setVideoEncodingBitRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VIDEO_BIT_RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 5 Mbps&lt;/span&gt;
                &lt;span class="nf"&gt;setVideoFrameRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VIDEO_FRAME_RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Create virtual display&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;displayMetrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayMetrics&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;densityDpi&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;displayMetrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;densityDpi&lt;/span&gt;
        &lt;span class="n"&gt;virtualDisplay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mediaProjection&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"RecordingDisplay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;VIDEO_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;VIDEO_HEIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Must match MediaRecorder video size&lt;/span&gt;
            &lt;span class="n"&gt;densityDpi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;DisplayManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mediaRecorder&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mediaRecorder&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RecordingService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Recording started!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;isRecording&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;e&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RecordingService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"startRecording failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;stopRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, do not forget to release resources properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;stopRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;isRecording&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;mediaRecorder&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;pfd&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pfd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="n"&gt;mediaRecorder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="n"&gt;virtualDisplay&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;virtualDisplay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="n"&gt;mediaProjection&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mediaProjection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Remove foreground state&lt;/span&gt;
    &lt;span class="c1"&gt;// Use STOP_FOREGROUND_REMOVE flag to remove both foreground state and notification.&lt;/span&gt;
    &lt;span class="c1"&gt;// This is the most common and complete way to stop foreground service.&lt;/span&gt;
    &lt;span class="nc"&gt;ServiceCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopForeground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="cm"&gt;/* service = */&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="cm"&gt;/* flags = */&lt;/span&gt; &lt;span class="nc"&gt;ServiceCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STOP_FOREGROUND_REMOVE&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;stopSelf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RecordingService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Recording stopped."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;stopRecording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above steps, we have completed a fully functional screen recording feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The combination of &lt;strong&gt;MediaRecorder + MediaProjection&lt;/strong&gt; is a powerful tool for implementing screen recording on Android. By encapsulating complex underlying implementations, it allows developers to achieve high-quality recording with minimal code.&lt;/p&gt;

&lt;p&gt;The complete sample code can be found in the screen recording section of the &lt;a href="https://github.com/Ilovecat1949/AudioAndVideoEditor" rel="noopener noreferrer"&gt;Audio and Video Editor&lt;/a&gt; project (note: the code is somewhat rough).&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/media/grow/media-projection#kotlin" rel="noopener noreferrer"&gt;Media Projection - Official Google Tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.google.cn/media/platform/mediarecorder?hl=zh-cn" rel="noopener noreferrer"&gt;MediaRecorder Overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>Efficiently Building App Permission Flows with the Compose Permission Request Template</title>
      <dc:creator>cat dog (running_in_the_storm)</dc:creator>
      <pubDate>Sun, 26 Oct 2025 07:29:39 +0000</pubDate>
      <link>https://forem.com/cat_dogrunning_in_the_s/efficiently-building-app-permission-flows-with-the-compose-permission-request-template-4gfe</link>
      <guid>https://forem.com/cat_dogrunning_in_the_s/efficiently-building-app-permission-flows-with-the-compose-permission-request-template-4gfe</guid>
      <description>&lt;h1&gt;
  
  
  Efficiently Building App Permission Flows with the Compose Permission Request Template
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Foreword
&lt;/h2&gt;

&lt;p&gt;In traditional Android development, permission requests rely on &lt;code&gt;requestPermissions()&lt;/code&gt; and &lt;code&gt;onRequestPermissionsResult()&lt;/code&gt; methods within the &lt;code&gt;Activity&lt;/code&gt;. Permission request logic is often concentrated in the &lt;code&gt;Activity&lt;/code&gt;, and the callback-based pattern results in fragmented code structures.&lt;/p&gt;

&lt;p&gt;With the rise of Jetpack Compose, we need a permission handling method that is more aligned with the declarative UI paradigm. The officially recommended &lt;strong&gt;&lt;code&gt;Activity Result API&lt;/code&gt;&lt;/strong&gt; (via &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;) decouples the request from the result, allowing our Compose functions to handle permissions in a &lt;strong&gt;state-driven&lt;/strong&gt; manner.&lt;/p&gt;

&lt;p&gt;While permission handling in Jetpack Compose follows declarative principles, writing the full sequence of &lt;strong&gt;Permission Check → Launcher Registration → Rationale Determination → Permanent Denial Handling&lt;/strong&gt; for every new permission remains time-consuming and repetitive.&lt;/p&gt;

&lt;p&gt;For efficient development, the goal of this article is to &lt;strong&gt;templatize and encapsulate&lt;/strong&gt; this flow. This article will provide a &lt;strong&gt;highly reusable, officially recommended&lt;/strong&gt; Compose permission request general template, helping developers quickly and elegantly set up permission request flows in any application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Article Objectives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Objective&lt;/th&gt;
&lt;th&gt;Key Content&lt;/th&gt;
&lt;th&gt;Problem Solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;General Templatization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Encapsulate permission request, state, rationale display, and permanent denial handling into &lt;strong&gt;one reusable Composable&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;Avoid repetitive writing of large amounts of permission check and callback logic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High Reusability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Design functions with flexible parameters, requiring only the permission name and the granted callback to be used.&lt;/td&gt;
&lt;td&gt;Applicable to all dangerous permission request scenarios in the app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State-Driven Design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The internal implementation follows Compose &lt;code&gt;State&lt;/code&gt; principles, ensuring responsive UI and accurate state.&lt;/td&gt;
&lt;td&gt;Guarantees that the permission UI and app state are always synchronized, preventing bugs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Official Recommended Core&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The template is based on &lt;strong&gt;&lt;code&gt;Activity Result API&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;shouldShowRationale&lt;/code&gt;&lt;/strong&gt; best practices.&lt;/td&gt;
&lt;td&gt;Ensures code robustness and future-proofing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Basic Setup: Manifest Declaration and Dependency Inclusion
&lt;/h2&gt;

&lt;p&gt;Regardless of the approach, all required permissions (Dangerous Permissions) must first be declared in the &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;manifest&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.WRITE_EXTERNAL_STORAGE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.READ_EXTERNAL_STORAGE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt;
        &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.MANAGE_EXTERNAL_STORAGE"&lt;/span&gt;
        &lt;span class="na"&gt;tools:ignore=&lt;/span&gt;&lt;span class="s"&gt;"ScopedStorage"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.POST_NOTIFICATIONS"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introduce Compose Activity Dependency
&lt;/h2&gt;

&lt;p&gt;Ensure your project includes the Activity Result API dependency for Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle (app level)&lt;/span&gt;
&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Must include; provides core APIs like rememberLauncherForActivityResult&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.activity:activity-compose:1.9.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Please use the latest version&lt;/span&gt;

    &lt;span class="c1"&gt;// If you need to handle multiple permissions, you can use this Contract&lt;/span&gt;
    &lt;span class="c1"&gt;// implementation("androidx.activity:activity-ktx:1.9.0")&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Core Practice: Building the Permission Request Template with &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The core of requesting permissions in Compose is using &lt;strong&gt;&lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;&lt;/strong&gt;. This is a Composable function responsible for registering an &lt;strong&gt;&lt;code&gt;ActivityResultLauncher&lt;/code&gt;&lt;/strong&gt; and automatically unregistering it when the component is removed. We build our permission request template based on this API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Activity Context Retrieval Utility
&lt;/h3&gt;

&lt;p&gt;Since the critical &lt;code&gt;shouldShowRationale&lt;/code&gt; API in permission requests relies on an &lt;code&gt;Activity&lt;/code&gt;, we first define a useful &lt;code&gt;Context&lt;/code&gt; extension function to retrieve it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Place this in your utility class for global access&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActivity&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ContextWrapper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseContext&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Permission requests must be made within an Activity Context&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;IllegalStateException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Permissions must be requested within an Activity Context."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Template Code Implementation
&lt;/h3&gt;

&lt;p&gt;We encapsulate all permission handling logic into a Composable function named &lt;code&gt;PermissionRequestTemplate&lt;/code&gt;. It requires three core parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;permission&lt;/code&gt;: The string name of the permission to request.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;content&lt;/code&gt;: The core feature UI to display after the permission is granted.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;rationaleContent&lt;/code&gt;: The UI to display when explaining the permission rationale.&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;PermissionRequestTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_permission&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
   &lt;span class="c1"&gt;// content: @Composable () -&amp;gt; Unit, // Content to show after permission is granted&lt;/span&gt;
    &lt;span class="n"&gt;rationaleContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;// Action to re-request after rationale explanation&lt;/span&gt;
        &lt;span class="n"&gt;onRequestPermission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// Action to navigate to settings after permanent denial&lt;/span&gt;
        &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="c1"&gt;// Action to navigate to settings after permanent denial&lt;/span&gt;
    &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lifecycleOwner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalLifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;

    &lt;span class="c1"&gt;// ① Core State: Tracks whether the permission has been granted&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;isPermissionGranted&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContextCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkSelfPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;PackageManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PERMISSION_GRANTED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Core State: Tracks whether the "Rationale Explanation" UI needs to be shown&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;showPermissionPromptDialog&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ② Register the permission request launcher&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;permissionLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLauncherForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RequestPermission&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;onResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;isGranted&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;isPermissionGranted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isGranted&lt;/span&gt; &lt;span class="c1"&gt;// Update state&lt;/span&gt;
            &lt;span class="c1"&gt;// If denied, check if Rationale should be shown (state needs update even if system dialog is closed)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isGranted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldShowRequestPermissionRationale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// ③ Permission Status Check and UI Rendering&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Status 1: Permission is granted -&amp;gt; Show core feature&lt;/span&gt;
        &lt;span class="n"&gt;isPermissionGranted&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;//content()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Status 2: Rationale needs to be shown -&amp;gt; Display Rationale Explanation UI&lt;/span&gt;
        &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;RationaleDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rationaleContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rationaleContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;onDismiss&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;onRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Re-launch the request&lt;/span&gt;
                    &lt;span class="n"&gt;permissionLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// Hide dialog after request is launched&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;onOpenSettings&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Status 3: Initial or Denied state -&amp;gt; Show request button/prompt&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// First request, or user has permanently denied (shouldShowRationale = false)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;showPermissionPromptDialog&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
                &lt;span class="nc"&gt;AlertDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;onDismissRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;showPermissionPromptDialog&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;confirmButton&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                            &lt;span class="c1"&gt;// Re-check if Rationale needs to be shown&lt;/span&gt;
                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ActivityCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldShowRequestPermissionRationale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                                    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="n"&gt;permission&lt;/span&gt;
                                &lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="c1"&gt;// First request, or user has permanently denied, launch request directly&lt;/span&gt;
                                &lt;span class="n"&gt;permissionLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;dismissButton&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;showPermissionPromptDialog&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;
                        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auxiliary Composable Encapsulation (Button and Rationale)
&lt;/h3&gt;

&lt;p&gt;To keep &lt;code&gt;PermissionRequestTemplate&lt;/code&gt; concise, the request button and rationale display are encapsulated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;RationaleDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rationaleContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onRequestPermission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;

    &lt;span class="nc"&gt;AlertDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onDismissRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;rationaleContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;onRequestPermission&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Action to re-request&lt;/span&gt;
                &lt;span class="n"&gt;onOpenSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onOpenSettings&lt;/span&gt; &lt;span class="c1"&gt;// Action to navigate to settings&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;confirmButton&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;dismissButton&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nc"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Template Usage: Concise and Efficient Call Example
&lt;/h3&gt;

&lt;p&gt;Using this template makes the business Composable simpler and more aligned with the declarative style.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;        &lt;span class="nc"&gt;PermissionRequestTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;permission&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WRITE_EXTERNAL_STORAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_write_permission&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;rationaleContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onOpenSettings&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="nc"&gt;Column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_and_write_permissions&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;fontWeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FontWeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="c1"&gt;// Guide user to request again&lt;/span&gt;
                        &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reauthorization&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="c1"&gt;// Guide user to settings page (handles permanent denial scenario)&lt;/span&gt;
                        &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onOpenSettings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;go_to_settings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;onOpenSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packageName&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ACTION_APPLICATION_DETAILS_SETTINGS&lt;/span&gt;
                &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;startActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through &lt;strong&gt;&lt;code&gt;PermissionRequestTemplate&lt;/code&gt;&lt;/strong&gt; and its auxiliary Composable, we achieve the goals for Compose permission requests: &lt;strong&gt;standardized flow, centralized logic, and minimized code.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Differences in Different Versions
&lt;/h3&gt;

&lt;p&gt;Unfortunately, permission checking and requesting methods in Android vary across different system versions. This means developers cannot exclusively rely on the &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt; code for all permission request logic; code implementation differs between versions.&lt;/p&gt;

&lt;p&gt;Taking storage permission as an example (Google officially discourages apps from obtaining full storage access, recommending their file picker instead): Before Android 11, storage permission was divided into &lt;code&gt;Manifest.permission.WRITE_EXTERNAL_STORAGE&lt;/code&gt; (write) and &lt;code&gt;Manifest.permission.READ_EXTERNAL_STORAGE&lt;/code&gt; (read), and could be checked directly using &lt;code&gt;checkSelfPermission&lt;/code&gt; and &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check permission&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;ContextCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkSelfPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;PackageManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PERMISSION_GRANTED&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Register permission request launcher&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;permissionLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLauncherForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RequestPermission&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;onResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;isGranted&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;isPermissionGranted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isGranted&lt;/span&gt; &lt;span class="c1"&gt;// Update state&lt;/span&gt;
        &lt;span class="c1"&gt;// If denied, check if Rationale should be shown (state needs update even if system dialog is closed)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isGranted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;showRationaleDialog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityCompat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldShowRequestPermissionRationale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Request&lt;/span&gt;
&lt;span class="n"&gt;permissionLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting with Android 11, the applicable method for checking and requesting changes, requiring navigation to the settings screen for authorization instead of using the launcher.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check storage permission&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isExternalStorageManager&lt;/span&gt;&lt;span class="p"&gt;()){&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Navigate to settings screen for authorization&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packageName&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION&lt;/span&gt;
&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;startActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The details of permission management on Android can be quite complex. We hope the template provided in this article is helpful to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;a href="https://developer.android.google.cn/training/permissions/requesting?hl=zh-cn" rel="noopener noreferrer"&gt;Request runtime permissions Google official documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt; &lt;a href="https://developer.android.google.cn/codelabs/android-privacy-codelab?hl=zh-cn" rel="noopener noreferrer"&gt;Codelab demonstrating privacy best practices&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>Tired of complicated compilation?Integrate AeroFFmpeg with one click to make Android audio and video development easier!</title>
      <dc:creator>cat dog (running_in_the_storm)</dc:creator>
      <pubDate>Sat, 20 Sep 2025 06:27:17 +0000</pubDate>
      <link>https://forem.com/cat_dogrunning_in_the_s/tired-of-complicated-compilationintegrate-aeroffmpeg-with-one-click-to-make-android-audio-and-728</link>
      <guid>https://forem.com/cat_dogrunning_in_the_s/tired-of-complicated-compilationintegrate-aeroffmpeg-with-one-click-to-make-android-audio-and-728</guid>
      <description>&lt;h1&gt;
  
  
  Tired of complicated compilation？Integrate AeroFFmpeg with one click to make Android audio and video development easier!
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;As an Android developer, have you ever needed to implement audio and video features in your project, but been put off by FFmpeg's complex compilation process? To use FFmpeg on Android, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download the FFmpeg source code, possibly including various dependent libraries.&lt;/li&gt;
&lt;li&gt;Configure a complex cross-compilation environment, such as the NDK and compiler.&lt;/li&gt;
&lt;li&gt;Write lengthy compilation scripts to handle compatibility issues with different CPU architectures and operating systems.&lt;/li&gt;
&lt;li&gt;Have to start all over again with every FFmpeg version update.&lt;/li&gt;
&lt;li&gt;This process can be a nightmare, time-consuming and labor-intensive, and prone to errors if you're not careful!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article introduces AeroFFmpeg, the open-source FFmpeg Android SDK project. It compiles FFmpeg into a simple, easy-to-use AAR (Android Archive) file, providing an out-of-the-box solution for Android developers. With just a few lines of code, Android developers can integrate powerful FFmpeg features into their Android projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimalistic AeroFFmpeg Integration
&lt;/h2&gt;

&lt;p&gt;Currently, AeroFFmpeg has a minSdk of 24 and a targetSdk of 34. It is compiled based on FFmpeg 4.2.9 and supports MP3, H.264, and H.265. Currently, AeroFFmpeg only supports the arm64-v8a and x86 architectures and does not yet support 16KB page sizes. Once AeroFFmpeg is integrated into your project, you can immediately use the FFmpeg command line. The following describes how to integrate and use AeroFFmpeg.&lt;/p&gt;

&lt;h3&gt;
  
  
  Importing the AeroFFmpeg AAR File
&lt;/h3&gt;

&lt;p&gt;Since AeroFFmpeg has not yet been released to the Maven repository, you will need to integrate the AAR file directly by importing it. The AAR file address is as follows:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Ilovecat1949/AeroFFmpeg/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AeroFFmpeg/releases/tag/v1.0.0&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the AAR file to the libs folder under the app module in your Android project. If the libs folder does not exist, create one manually.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1przjs4x0eu9ofa3fvod.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1przjs4x0eu9ofa3fvod.jpg" alt="Importing the AAR file" width="497" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the build.gradle file
Open the app module's build.gradle file. You need to configure two sections: repositories and dependencies.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repositories {
    flatDir {
        dirs("libs")
    }
}

implementation(files("libs/AeroFFmpeglib-release.aar"))


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The declaration repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) in the settings.gradle.kts file will prevent the module from being added locally. If present, replace this declaration with repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2zvm2u2smkv3twssj2t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2zvm2u2smkv3twssj2t.jpg" alt="Replace Declaration" width="733" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sync the Project
Click Sync Now. Gradle will automatically download the .aar file and all its dependencies from the specified Maven repository and add it to your project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these steps, you've integrated AeroFFmpeg into your project.&lt;/p&gt;
&lt;h3&gt;
  
  
  AeroFFmpeg Usage and Scenarios
&lt;/h3&gt;

&lt;p&gt;AeroFFmpeg uses a singleton model and supports the FFmpeg command line, which executes asynchronously in a child thread. Note that AeroFFmpeg does not support multi-threaded concurrent execution of the FFmpeg command line; a new command line task cannot be started before the previous one has completed. If you need to support concurrent audio and video tasks, consider using Android multi-process programming to call different AeroFFmpeg instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//Start an FFmpeg command line task
//Returns a result: 0 for success, -1 for failure
val result = AeroFFmpeg.start("ffmpeg -i input.mp4 output.mp4")

//Get the current task status: 0: Executing the task or not executing at all; 1: Task completed without error; -1: Task completed with error
AeroFFmpeg.getTaskState()

//Cancel the task
AeroFFmpeg.cancelTask()

//Get the current task progress. Returns a Double value in milliseconds.
AeroFFmpeg.getProgressTime()

//Process log information through the log callback function
AeroFFmpeg.setLogListener (
object: OnLogListener {
override fun onLog(level: Int, message: String) {
Log.d("AeroFFmpeg.setLogListener","$level,$message")

}
}
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Completing Background Audio and Video Tasks with AeroFFmpeg and WorkManager
&lt;/h2&gt;

&lt;p&gt;Next, we'll develop an application that performs background audio and video tasks based on AeroFFmpeg and WorkManager. WorkManager, as the officially recommended background task API, offers advantages such as high reliability, persistent execution, and high energy efficiency. For detailed usage of WorkManager, please refer to my previous article.&lt;br&gt;
First, define a Worker class that executes the FFmpeg command line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class FFmpegTaskWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    // 重写 getForegroundInfo() 以提供前台服务通知信息
    override suspend fun getForegroundInfo(): ForegroundInfo {
        return ForegroundInfo(
            FFMPEG_CMD_ID,
            notificationBuilder.setContentText("开始执行任务...").build()
        )
    }

    override suspend fun doWork(): Result {
        setForeground(getForegroundInfo())
        val cmd = inputData.getString(FFMPEG_CMD)
        return withContext(Dispatchers.IO) {
            try {
            try {
                if(AeroFFmpeg.start(cmd!!)&amp;lt;0){
                    return@withContext Result.failure()
                }
                AeroFFmpeg.start(cmd)
                while(AeroFFmpeg.getTaskState()==0){
                    notificationBuilder.setContentText("任务进度：${formatTimeFromMillis(AeroFFmpeg.getProgressTime())}")
                    setProgress(AeroFFmpeg.getProgressTime())
                    if (ActivityCompat.checkSelfPermission(
                            applicationContext,
                            Manifest.permission.POST_NOTIFICATIONS
                        ) == PackageManager.PERMISSION_GRANTED
                    ) {
                        notificationManager.notify(FFMPEG_CMD_ID, notificationBuilder.build())
                    }
                    delay(100)
                }
                if(AeroFFmpeg.getTaskState()==1){
                    notificationBuilder.setContentText("执行成功")
                    if (ActivityCompat.checkSelfPermission(
                            applicationContext,
                            Manifest.permission.POST_NOTIFICATIONS
                        ) == PackageManager.PERMISSION_GRANTED
                    ) {
                        notificationManager.notify(FFMPEG_CMD_ID, notificationBuilder.build())
                    }
                    return@withContext Result.success()
                }
                else{
                    notificationBuilder.setContentText("执行失败")
                    if (ActivityCompat.checkSelfPermission(
                            applicationContext,
                            Manifest.permission.POST_NOTIFICATIONS
                        ) == PackageManager.PERMISSION_GRANTED
                    ) {
                        notificationManager.notify(FFMPEG_CMD_ID, notificationBuilder.build())
                    }
                    return@withContext Result.failure()
                }
            } catch (e: Exception) {
                e.printStackTrace()
                // Task failed, update notification
                notificationBuilder.setContentText("执行失败").setProgress(0, 0, false)
                return@withContext Result.failure()
            } finally {
                // Ensure notification is cancelled
                if (ActivityCompat.checkSelfPermission(
                        applicationContext,
                        Manifest.permission.POST_NOTIFICATIONS
                    ) == PackageManager.PERMISSION_GRANTED
                ) {
                    notificationManager.cancel(FFMPEG_CMD_ID)
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define a method to start the task and encapsulate the process of submitting the task to WorkManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun startFFmpegTask(context: Context, cmd: String) {

    val data = workDataOf(FFMPEG_CMD to cmd)
    // Set constraints to only download when connected to a network
    val constraints = Constraints.Builder()
        //.setRequiredNetworkType(NetworkType.CONNECTED)
        .build()

    // Create a OneTimeWorkRequest for the download
    val ffmpegTaskRequest = OneTimeWorkRequestBuilder&amp;lt;FFmpegTaskWorker&amp;gt;()
        .setConstraints(constraints)
        .setInputData(data)
        .addTag(FFMPEG_TASK_TAG)
        .build()
    // Enqueue the work request
    WorkManager.getInstance(context).enqueue(ffmpegTaskRequest)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through the LiveData class, the status and progress of tasks can be observed and displayed in real time, achieving synchronization between background task information and UI display information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    val context = LocalContext.current
    val workManager = WorkManager.getInstance(context)
    var cmd by remember { mutableStateOf("") }
    val workInfos: LiveData&amp;lt;List&amp;lt;WorkInfo&amp;gt;&amp;gt; = remember {
        workManager.getWorkInfosByTagLiveData(FFMPEG_TASK_TAG)
    }
    val ffmpegTasks by workInfos.observeAsState(initial = emptyList())
    ...

        LazyColumn(
            modifier = Modifier.fillMaxSize()
        ) {
            items(ffmpegTasks) { workInfo -&amp;gt;
                FFmpegTaskItem(workInfo)
            }
        }    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting application is shown below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcas9w11i2nk55hqoirzn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcas9w11i2nk55hqoirzn.jpg" alt="Execute FFmpeg command line task in the background" width="378" height="832"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thus, using AeroFFmpeg, we can easily implement audio and video features in Android apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;FFmpeg is a powerful open-source audio and video processing tool that supports transcoding, editing, recording, streaming, filtering, and more. With AeroFFmpeg, you can bring FFmpeg's audio and video capabilities to your Android app without complex NDK configuration or lengthy compilation scripts. I will continue to update and improve AeroFFmpeg.&lt;br&gt;
The AeroFFmpeg repository is as follows:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Ilovecat1949/AeroFFmpeg" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AeroFFmpeg&lt;/a&gt;&lt;br&gt;&lt;br&gt;
The AeroFFmpeg+WorkManager sample code is in the WorkDownloader project in the following repository:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Ilovecat1949/AndroidApps" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AndroidApps&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/projects/android-library?hl=en-us" rel="noopener noreferrer"&gt;Google Tutorial on Creating an Android Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.google.cn/develop/background-work?hl=en-us" rel="noopener noreferrer"&gt;Official Android Background Work Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ffmpeg.org/ffmpeg.html" rel="noopener noreferrer"&gt;Official ffmpeg Command Line Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>android</category>
      <category>opensource</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Say goodbye to tedious work and use WorkManager to solve the problem of background work</title>
      <dc:creator>cat dog (running_in_the_storm)</dc:creator>
      <pubDate>Sat, 13 Sep 2025 05:40:26 +0000</pubDate>
      <link>https://forem.com/cat_dogrunning_in_the_s/say-goodbye-to-tedious-work-and-use-workmanager-to-solve-the-problem-of-background-work-3ojc</link>
      <guid>https://forem.com/cat_dogrunning_in_the_s/say-goodbye-to-tedious-work-and-use-workmanager-to-solve-the-problem-of-background-work-3ojc</guid>
      <description>&lt;h1&gt;
  
  
  Say goodbye to tedious work and use WorkManager to solve the problem of background work
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;Executing background tasks is a complex part of Android development, requiring numerous considerations. First and foremost is battery life. Frequent background tasks consume a significant amount of battery life, a problem unbearable for both users and the system. To combat the rampant and surreptitious background work of Android apps, Google has introduced various background restriction mechanisms (Doze Mode, App Standby, and Background Execution Limits). Newer versions of Android have tightened background management, creating compatibility challenges for developers. Most importantly, developers struggle to ensure the reliability of their apps' background tasks, as the system often pauses or kills them for various reasons.&lt;/p&gt;

&lt;p&gt;WorkManager, a background task manager within the Android Jetpack family, is the officially recommended API for persistent work and general background processing. This article will demonstrate the use of WorkManager using the example of developing a downloader. The downloader code can be found in the following repository: &lt;a href="https://github.com/Ilovecat1949/AndroidApps" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AndroidApps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You are also welcome to check out my audio and video development project: &lt;a href="https://github.com/Ilovecat1949/AudioAndVideoEditor" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AudioAndVideoEditor&lt;/a&gt;. This is an open source Android audio and video editor that supports ffmpeg command line, video encoding compression and format conversion, video cropping and speed change, and other audio and video functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WorkManager is the best choice
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Officially Endorsed and Reliable: As Google's officially recommended background task solution, WorkManager continues to receive support and optimization, with a simple and consistent API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Persistent and Reliable Execution: This is WorkManager's greatest strength. Work execution is maintained even if the user navigates away from the screen, exits the app, or reboots the device. For failed tasks, WorkManager provides flexible retry policies, including a configurable exponential backoff policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Energy Efficiency: WorkManager adheres to power-saving features and best practices like Doze and offers powerful scheduling capabilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flexible Constraints: Developers can set various constraints for tasks, allowing them to execute at the most appropriate time, saving battery and data usage.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Network Status: Execute only when connected to Wi-Fi.&lt;/p&gt;

&lt;p&gt;Charging Status: Execute only when the device is charging.&lt;/p&gt;

&lt;p&gt;Storage Space: Execute only when sufficient storage space is available.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Diverse Task Types: Supports one-time, periodic, and chained tasks to meet the diverse needs of developers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Core Concepts and Practices: How does WorkManager work?
&lt;/h2&gt;

&lt;p&gt;WorkManager's design philosophy is very clear, breaking down background tasks into three core components:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Worker: Unit of Work&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concept: WorkManager abstracts background tasks into independent Workers. Each Worker is responsible for performing a specific task, such as uploading logs, synchronizing data, or downloading files.&lt;/p&gt;

&lt;p&gt;Implementation: Inherit the Worker class and override the doWork() method. Implement your background task logic in this method.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Task Request: WorkRequest&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concept: A WorkRequest describes how a task should be executed, including constraints, execution delay, and other information.&lt;/p&gt;

&lt;p&gt;Practice: Create a task request using OneTimeWorkRequest (for one-time tasks) or PeriodicWorkRequest (for periodic tasks) and set constraints using the Constraints builder.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Task Scheduling: WorkManager&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concept: WorkManager is the scheduling center for background tasks. It receives WorkRequests and decides when to execute a Worker based on constraints and system status.&lt;/p&gt;

&lt;p&gt;Practice: Submit tasks to the WorkManager using WorkManager.getInstance(context).enqueue(workRequest) .&lt;/p&gt;

&lt;h2&gt;
  
  
  Talking with code: Developing a downloader using WorkManager
&lt;/h2&gt;

&lt;h3&gt;
  
  
  design
&lt;/h3&gt;

&lt;p&gt;Before developing, we need to first consider the nature of our application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Develop it in Kotlin and develop the UI using Jetpack Compose.&lt;/li&gt;
&lt;li&gt;Be able to request the necessary permissions for downloads, such as network permissions and file read/write permissions.&lt;/li&gt;
&lt;li&gt;The interface should allow users to enter a download link and initiate the download.&lt;/li&gt;
&lt;li&gt;Provide a download task list to display download progress and status.&lt;/li&gt;
&lt;li&gt;Support download progress notifications.
The importance of notifications is crucial. Long-running tasks that require continuous network or CPU access require a "pass" to inform the system of their importance. To address this, we need to upgrade the download task to a "foreground service." A foreground service notifies the system that the task is in progress and is visible to the user (via notifications), preventing the system from easily terminating it.
### Code Implementation
We first need to declare the required permissions in AndroidManifest.xml. For Android 13 and later Android versions, we need to explicitly declare the notification permission.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;uses-permission android:name="android.permission.POST_NOTIFICATIONS" /&amp;gt;
    &amp;lt;!-- 声明网络权限，这是下载功能所必需的 --&amp;gt;
    &amp;lt;uses-permission android:name="android.permission.INTERNET" /&amp;gt;
    &amp;lt;uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /&amp;gt;
    &amp;lt;uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to check and request permissions at runtime, and create a notification channel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -&amp;gt;
            if (isGranted) {
                // Permission granted. Continue with the app flow.
            } else {
                Toast.makeText(this, "需要通知权限才能显示下载进度", Toast.LENGTH_LONG).show()
            }
        }
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.O) {
            val name = "下载通知"
            val descriptionText = "显示下载文件的进度和状态"
            val importance = NotificationManagerCompat.IMPORTANCE_LOW
            val channel = android.app.NotificationChannel(DOWNLOAD_NOTIFICATION_CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            val notificationManager: android.app.NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
        if (Build.VERSION.SDK_INT &amp;lt; Build.VERSION_CODES.R){
            val permission = arrayOf(
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
            val requestPermissionLauncher =
                this.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -&amp;gt;

                }
            permission.forEach {
                if(! (this.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED)){
                    requestPermissionLauncher.launch(it)
                }
            }
        }
        else{
            if(!Environment.isExternalStorageManager()){
                val builder = android.app.AlertDialog.Builder(this)
                    .setMessage("需要获取文件读写权限")
                    .setPositiveButton("ok") { _, _ -&amp;gt;
                        val packageName = this.packageName
                        val intent = Intent()
                        intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
                        intent.data = Uri.fromParts("package", packageName, null)
                        ContextCompat.startActivity( this, intent, null)
                    }
                    .setNeutralButton("稍后再问"){ _, _ -&amp;gt;

                    }
                builder.show()
            }
        }


        // 请求通知权限 (适用于 Android 13 及以上版本)
        if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.TIRAMISU) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }

        // 创建通知渠道
        createNotificationChannel()    
    ...    
    }        
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define a work class based on your own needs, and write the specific logic of executing the task in this work class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DownloadWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    private val notificationManager = NotificationManagerCompat.from(appContext)
    private val notificationBuilder = NotificationCompat.Builder(applicationContext, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
        .setContentTitle("下载中")
        .setSmallIcon(android.R.drawable.stat_sys_download)
        .setPriority(NotificationCompat.PRIORITY_LOW)
        .setOngoing(true)

    // 最大文件名长度
    private val MAX_FILENAME_LENGTH = 50

    // 重写 getForegroundInfo() 以提供前台服务通知信息
    override suspend fun getForegroundInfo(): ForegroundInfo {
        return ForegroundInfo(
            DOWNLOAD_NOTIFICATION_ID,
            notificationBuilder.setContentText("开始下载...").build()
        )
    }

    override suspend fun doWork(): Result {
     //文件下载的具体逻辑写在这里面 
     ...
    }

    // Helper function to set progress for the UI
    private fun setProgress(progress: Int) {
        val progressData = workDataOf("progress" to progress)
        setProgressAsync(progressData)
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We obtain a WorkManager instance through the context, which directly handles all tasks related to the task database and scheduling system. WorkManager uses a Room database (an abstraction layer for SQLite databases) in the application's private storage space to achieve task persistence and state management. We can directly access all task information from the WorkManager instance. Combined with LiveData, the application interface can observe and display task status and progress in real time, achieving synchronization between background tasks and the UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    val workManager = WorkManager.getInstance(context)
    ...
    val workInfos: LiveData&amp;lt;List&amp;lt;WorkInfo&amp;gt;&amp;gt; = remember {
        workManager.getWorkInfosByTagLiveData(DOWNLOAD_WORK_TAG)
    }
    val downloadTasks by workInfos.observeAsState(initial = emptyList())
    ...
    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ) {
        items(downloadTasks) { workInfo -&amp;gt;
            DownloadTaskItem(workInfo)
        }
    }    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define a download start method to encapsulate the process of submitting tasks to WorkManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun startDownload(context: Context, url: String) {
    // 1. 创建 WorkRequest，指定要执行的 Worker
    val downloadRequest = OneTimeWorkRequestBuilder&amp;lt;DownloadWorker&amp;gt;()
        .setInputData(workDataOf(DOWNLOAD_WORK_URL to url)) // 2. 传递输入数据
        .addTag(DOWNLOAD_WORK_TAG) // 3. 添加标签以便跟踪
        .build()

    // 4. 将任务加入队列
    WorkManager.getInstance(context).enqueue(downloadRequest)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above is the idea of ​​using WorkManager to develop a downloader.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwfubb1bw53b1vy7nioi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwfubb1bw53b1vy7nioi.jpg" alt="WorkDownloader" width="375" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking ahead: New trends in Android background tasks and Google policies
&lt;/h2&gt;

&lt;p&gt;Google has been tightening the permissions for background tasks to protect user privacy and device battery life. Future Android versions may introduce even stricter background execution restrictions, making traditional approaches like Services and BroadcastReceivers increasingly difficult to use.&lt;br&gt;
Against this backdrop, WorkManager, as the officially recommended solution, will become increasingly important. It helps us comply with Google's policies while ensuring reliable task execution.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Summarize
&lt;/h2&gt;

&lt;p&gt;Let's review the advantages of WorkManager: reliability, compatibility, durability, and flexibility. It frees developers to focus on business logic without having to worry about complex background task adaptation and compatibility issues.&lt;/p&gt;

&lt;p&gt;If you're still struggling with background tasks, it's time to embrace WorkManager. It will not only make your app more stable but also your development more efficient.&lt;/p&gt;

&lt;p&gt;The downloader code can be found in the following repository: &lt;a href="https://github.com/Ilovecat1949/AndroidApps" rel="noopener noreferrer"&gt;https://github.com/Ilovecat1949/AndroidApps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please check out my audio and video development project: &lt;a href="https://ilovecat1949.github.io/AudioAndVideoEditor/" rel="noopener noreferrer"&gt;https://ilovecat1949.github.io/AudioAndVideoEditor/&lt;/a&gt;. This is an open-source Android audio and video editor that supports various audio and video functions, including ffmpeg command-line functionality, video encoding and compression, format conversion, video cropping, and speed changes.  &lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://developer.android.google.cn/develop/background-work?hl=en-us" rel="noopener noreferrer"&gt;Android Background Worker Official Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.google.cn/codelabs/android-workmanager" rel="noopener noreferrer"&gt;WorkManager Official Course&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>android</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Create your own personal website for free — A guide to using GitHub Pages</title>
      <dc:creator>cat dog (running_in_the_storm)</dc:creator>
      <pubDate>Sat, 30 Aug 2025 00:28:12 +0000</pubDate>
      <link>https://forem.com/cat_dogrunning_in_the_s/create-your-own-personal-website-for-free-a-guide-to-using-github-pages-1f7a</link>
      <guid>https://forem.com/cat_dogrunning_in_the_s/create-your-own-personal-website-for-free-a-guide-to-using-github-pages-1f7a</guid>
      <description>&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;I've been wanting to create a website for my independently developed app, AudioAndVideoEditor. Setting up a server, domain name, and developing the website from scratch is obviously expensive. GitHub offers free static web hosting, allowing you to launch your website in just a few steps (compared to developing a website from scratch). This article will explain how to use GitHub Pages, drawing on my own experience.&lt;/p&gt;

&lt;p&gt;You're welcome to follow my GitHub Pages: &lt;a href="https://ilovecat1949.github.io/AudioAndVideoEditor/" rel="noopener noreferrer"&gt;https://ilovecat1949.github.io/AudioAndVideoEditor/&lt;/a&gt; .It's an open-source Android audio and video editor that supports various audio and video features, including ffmpeg command-line functionality, video encoding and compression, format conversion, video cropping, and speed changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Pages advantages and limitations
&lt;/h2&gt;

&lt;p&gt;Everyone knows that GitHub offers free code hosting, but not many people know that you can use GitHub Pages to build a personal website. Let me briefly explain the advantages and limitations of GitHub Pages.   &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zero cost. This is the most obvious advantage. No server, domain, or hosting fees are required.
&lt;/li&gt;
&lt;li&gt;Easy to use. No development knowledge is required; you can build a website using ready-made templates. Especially with the addition of AI programming, it's even easier to customize your desired interface.
&lt;/li&gt;
&lt;li&gt;Stability and reliability. GitHub is reliable. Once you build your website, you don't have to worry about security issues like website takedowns or hacker attacks. GitHub's global CDN acceleration ensures lightning-fast page loads and supports HTTPS for secure encryption.&lt;/li&gt;
&lt;li&gt;GitHub integration: Websites are directly hosted on GitHub repositories, making version control, collaboration, and sharing easy.
&lt;/li&gt;
&lt;li&gt;With a GitHub Pages website, you can create an online resume for job hunting.
According to the GitHub Terms of Service, our use of GitHub Pages is subject to the following restrictions:&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each GitHub account can only create one user or organization site.&lt;br&gt;&lt;br&gt;
A GitHub Pages page has a URL formatted as username.github.io/projectname or organization.github.io/projectname. This restriction means that an account can only have one GitHub Pages site with username.github.io or organization.github.io. You can create different GitHub Pages for multiple projects, but they all have the same prefix: username.github.io or organization.github.io.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The recommended size limit for source code repositories is 1 GB, and published sites cannot exceed 1 GB.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deployment timeout is 10 minutes.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is a soft limit of 100 GB of monthly bandwidth.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A maximum of 10 builds per hour is allowed (sites published using custom GitHub Actions workflows are exempt from this limit).&lt;br&gt;&lt;br&gt;
Using GitHub Pages' default build method (for example, through the Publish Source option in GitHub Pages settings), your site can only trigger a maximum of 10 builds per hour.&lt;br&gt;&lt;br&gt;
However, if you use a custom GitHub Actions workflow to build and publish your site, this limit does not apply. In other words, custom GitHub Actions workflows can bypass the 10 builds per hour limit, allowing you to build and release your site more frequently as needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate limiting may apply to ensure quality of service, and triggering a rate limit will result in an HTTP status code 429 response.   &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Additionally, GitHub Pages is not intended for commercial transactions, SaaS services, or other sensitive transactions (such as sending passwords or credit card information).      &lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub Pages Setup Guide
&lt;/h2&gt;

&lt;p&gt;GitHub Pages relies on a public GitHub repository. Therefore, before building GitHub Pages, you'll need to register for a GitHub account and create a GitHub repository. I'll be building on my open-source project, AudioAndVideoEditor. Once you have a repository, follow the steps below to activate GitHub Pages.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk1ww8vgpcpefc2byou1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk1ww8vgpcpefc2byou1.jpg" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to your repository and click the "Settings" tab.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find the "Pages" option in the left menu.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under "Source," select "Deploy from a branch," then select the "main" branch (the default), and click "Save."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After a few minutes, refresh the page. You'll see the generated website URL: &lt;a href="https://username.github.io/projectname" rel="noopener noreferrer"&gt;https://username.github.io/projectname&lt;/a&gt;. For example, mine is &lt;a href="https://ilovecat1949.github.io/AudioAndVideoEditor/" rel="noopener noreferrer"&gt;https://ilovecat1949.github.io/AudioAndVideoEditor/&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GitHub repositories typically have a README file. If no other webpage files are present, GitHub Pages will display your README file. To build a fancy and attractive interface, we usually need to create or upload a webpage file. Here's how.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the repository (root directory), click "Add file" &amp;gt; "Create new file."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Name the file "index.html" and enter the following simple code:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;我的个人网页&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;欢迎来到我的世界！&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;这里是我的介绍：热爱编程、旅行和咖啡。&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit your code, and refresh the site after a few seconds to see the updated content. This time, the contents of index.html are displayed.&lt;/p&gt;

&lt;p&gt;Okay, that's the general idea and steps for using GitHub Pages. But it's obviously not enough to create a polished interface. Next, let's explore building a blog using a Jekyll template.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Branch Build and Actions Build
&lt;/h2&gt;

&lt;p&gt;Before using Jekyll, we need to understand some basics about GitHub Pages builds. In the steps above, we can either choose a code branch as the source for the build or use GitHub Actions.&lt;br&gt;
Building from a branch is the most traditional and simplest method. It works by specifying a branch (usually gh-pages or main), and GitHub Pages automatically uses the content from that branch as your website. Its advantages are simplicity and zero configuration. You simply push your static website files (HTML, CSS, and JS) directly to that branch (usually in the root directory). Its disadvantage is that there's no automated build process. If you use Jekyll, you'll need to first run jekyll build locally to generate a _site directory, then push all the files in _site to the GitHub Pages branch (usually in the root directory). This is tedious and prone to errors.&lt;br&gt;
Building with Actions (GitHub Actions) is a more modern and powerful method. How it works is that you create a GitHub Actions workflow. When code is pushed to a specified branch, this workflow automatically runs, completes the Jekyll build for you in the cloud, and publishes the generated website files to GitHub Pages. Its advantage is that it is fully automated. You only need to push your Jekyll source code (such as .md files and layout files) to the repository, and all the rest of the work (building and deploying) is automatically completed by GitHub Actions. You don't need to run jekyll build locally. The disadvantage is that it requires writing and configuring a YAML workflow file, which may be a bit complicated for beginners. However, when you choose GitHub Actions to build, GitHub will prompt you to select Jekyll Workflow for the build. Just follow GitHub's prompts and it will automatically generate a .github/workflows/jekyll-gh-pages.yml workflow file for you.     &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi9d8zoaj1m7ldebc9c1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi9d8zoaj1m7ldebc9c1.jpg" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building with a branch is like manually uploading your project. You prepare the final product and then submit it to GitHub. The HTML, Markdown, and other files in the project's root directory serve as the source code for the webpage.&lt;br&gt;
Building with Actions is like hiring a full-time automated worker. You provide the raw material (the Jekyll source code) and it completes all the work for you and delivers the final product. The code generated by the build is stored in the _site directory, and the files in the _site directory serve as the source code for the webpage.&lt;br&gt;
I personally chose to build with Actions. I built the webpage locally using Jekyll build for debugging, then uploaded it to GitHub, triggering the workflow to build and publish it again.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub Pages+Jekyll
&lt;/h2&gt;

&lt;p&gt;First, let's talk about Jekyll. Jekyll is a powerful static website generator. It automatically generates a complete static website from your Markdown articles, combined with beautifully designed themes and templates. You focus on creating your content, while Jekyll handles the rest of the polish and layout. We no longer need to fiddle with Markdown to generate web pages; powerful AI programming can now directly generate web files that meet our needs. What we really need are Jekyll's themes, templates, local preview, and real-time debugging features.&lt;/p&gt;
&lt;h3&gt;
  
  
  Installing Jekyll
&lt;/h3&gt;

&lt;p&gt;It's best to develop and test locally before uploading. To use Jekyll locally, you need to install some dependencies.&lt;br&gt;
Jekyll is developed in Ruby, so you need to install Ruby and Bundler first.&lt;br&gt;
Installing Ruby:&lt;br&gt;
Windows users: We recommend using RubyInstaller for Windows. During installation, select the "Add Ruby executables to your PATH" option.&lt;br&gt;
macOS users: Ruby is usually pre-installed. You can check the version by typing "ruby -v" in the terminal.&lt;br&gt;
Linux users: Use a package manager (e.g., "sudo apt-get install ruby-full").&lt;br&gt;
Installing Bundler:&lt;br&gt;
Bundler is a Ruby package manager that manages and installs the dependencies required for Jekyll projects. In a terminal or command prompt, run the following Bash command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem install bundler jekyll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will install both Bundler and Jekyll.&lt;br&gt;
The above installation is expected to take up 1GB of disk space.&lt;/p&gt;
&lt;h3&gt;
  
  
  Clone your repository locally
&lt;/h3&gt;

&lt;p&gt;Now, you'll need to clone the website repository you created on GitHub to your local computer.&lt;br&gt;
Install Git: If you don't have Git installed on your computer, you'll need to install it first.&lt;br&gt;
Clone the repository: Open a terminal or Git Bash, navigate to the folder where you want to store your project, and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/YourGitHubusername/projectname.git
cd projectname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you're in your project folder, you can start your website locally.&lt;br&gt;
Install dependencies: Run the following command to install the project's required dependencies (including Jekyll and the theme plugin):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run may take some time.&lt;br&gt;
Starting a local server: Now, run the following command to start a local server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle exec jekyll serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the command completes, you'll see a local URL in your terminal, typically &lt;a href="http://127.0.0.1:4000" rel="noopener noreferrer"&gt;http://127.0.0.1:4000&lt;/a&gt;. Open this URL in your browser and you'll see your website.&lt;br&gt;
Now, navigate to your repository directory and you'll notice a number of new files and directories. In the root directory, there's a file called index_markdown.md, which serves as the default entry point for the entire website. As mentioned above, we won't bother with generating Markdown pages. Instead, we'll change index_markdown.md to index_markdown.txt, which will disable it. Then, add an index.html file (if one isn't already there) so that Jekyll uses it as the entry point for the website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrxi0yvjuuyfb1qrfb16.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrxi0yvjuuyfb1qrfb16.jpg" alt=" " width="582" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, you just need to let AI write web page code based on Jekyll to meet your needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5qg3joeqw3ehkg8hn1u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5qg3joeqw3ehkg8hn1u.jpg" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After writing the code, you can preview the effect locally and submit the code if it is OK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add .
git commit -m "Your submission information"
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After submitting, GitHub Pages will automatically deploy the website for you again (selecting Actions Build will rebuild and then deploy it), so that your online website is completely synchronized with the local one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarize
&lt;/h2&gt;

&lt;p&gt;Using GitHub Pages, you can create a free website to promote yourself, your projects, and your products, creating a personal "digital business card" that might even add a splash to your resume. Paired with the ever-popular AI programming, creating a satisfying static webpage is easy. Open GitHub now and start your creative journey!&lt;/p&gt;

&lt;p&gt;Finally, I welcome everyone to follow my GitHub Pages: &lt;a href="https://ilovecat1949.github.io/AudioAndVideoEditor/" rel="noopener noreferrer"&gt;https://ilovecat1949.github.io/AudioAndVideoEditor/&lt;/a&gt; It's an open-source audio and video editor for Android, supporting various audio and video features, such as the ffmpeg command line, video encoding and compression, format conversion, video cropping, and speed changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  reference
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/pages" rel="noopener noreferrer"&gt;GitHub Pages Official Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jekyllrb.com/" rel="noopener noreferrer"&gt;Jekyll Official Website&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.ruby-lang.org/zh_cn/" rel="noopener noreferrer"&gt;Ruby Official Website&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>productivity</category>
      <category>github</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
