Double Eleven is here, let’s make an icon face-changing feature for my app

Author: Zhuifengxianchen

Preface

Perhaps you have also noticed that as Double 11 approaches, the application icons of e-commerce apps on mobile phones have been quietly replaced with double 11 exclusive icons, such as certain treasures and certain Dongs:

You might say, what's weird about this, isn't it enough to enable automatic updates in the app market?

Is it really?

To this end, I deliberately checked the current version of a certain treasure app on my mobile phone, and compared the icons on the historical version, and found that it did not correspond.

The default is the exclusive icon for the 88 member's day, but now the double 11 icon is displayed.

So, as a developer's sense of smell, you naturally want to guess how to achieve it from a technical point of view, and this is what this article wants to share with you.


Knowledge reserve

An alias of an Activity, used to instantiate the target Activity. The target must be in the same application as the alias and must be declared before the alias in the manifest.
Introduce several important attributes:
android:enabled: must be set to "true", the system can instantiate the target activity
through the alias android:icon: the icon of the target activity when presented to the user through the alias.
android:name: The unique name of the alias. Unlike the name of the target Activity, the alias name is arbitrary, and it does not refer to the actual class.
android:targetActivity: The name of the activity that can be activated by alias.

PackageManager#setComponentEnabledSetting

You can use the PackageManager to switch the enabled state on any component defined in the manifest file, including any Activity you want to enable or disable.

With the above knowledge reserves, it is time to analyze the specific scenarios of this demand.


Scene analysis

Take the e-commerce APP Double 11 event as an example. At a certain time before the Double 11 event (such as 10 days before), the event will start to warm up. At this time, the icon will be automatically replaced. After the end, it must be able to change back to the normal icon, and the process is required to be as insensitive to the user as possible, and it must not affect the user's normal use of the APP.

The specific function points to be achieved are: icon replacement, automatic operation, and user unawareness.


Scheme realization

1. Icon replacement: Disable the Launcher component, enable the Alias ​​component, and point the targetActivity to the original Launcher component.
2. Automatic operation: the specified date is converted to a timestamp and compared with the current timestamp, and the replacement operation is performed when the preset time is exceeded.
3. User unawareness: Try to choose the stage when the APP is inactive, such as when switching applications/returning to the desktop.


Code practice

First, we need to add elements to the AndroidManifest manifest file. The default is disabled. The name attribute is the only sign that we find this component, and the icon attribute is the icon resource we want to replace, and the SplashActivity of LANCHUER is used as the targetActivity attribute. The instantiated target Activity:

<activity android:name=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<!--88会员节专属Activity别名-->
<activity-alias
    android:name=".SplashAliasActivity"
    android:enabled="false"
    android:icon="@mipmap/ic_launcher_88"
    android:targetActivity=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

<!--双11专属Activity别名-->
<activity-alias
    android:name=".SplashAlias2Activity"
    android:enabled="false"
    android:icon="@mipmap/ic_launcher_11_11"
    android:targetActivity=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

Subsequently, our icon replacement work is regarded as a task, defining a data class:

/**
 * 切换图标任务
 */
data class SwitchIconTask (val launcherComponentClassName: String,  // 启动器组件类名
                           val aliasComponentClassName: String,  // 别名组件类名
                           val presetTime: Long,            // 预设时间
                           val outDateTime: Long)           // 过期时间

Define a LauncherIconManager singleton, responsible for icon replacement related work. Open the interface for adding icon switching tasks to verify the validity of the parameters:

/**
 * 启动器图标管理器
 */
object LauncherIconManager {

    /** 切换图标任务Map */
    private val taskMap: LinkedHashMap<String, SwitchIconTask> = LinkedHashMap()

    /**
     * 添加图标切换任务
     * @param newTasks 新任务,可以传多个
     */
    fun addNewTask(vararg newTasks: SwitchIconTask) {
        for (newTask in newTasks) {
            // 防止重复添加任务
            if (taskMap.containsKey(newTask.aliasComponentClassName)) return

            // 校验任务的预设时间和过期时间
            for (queuedTask in taskMap.values) {
                if (newTask.presetTime > newTask.outDateTime) throw IllegalArgumentException("非法的任务预设时间${newTask.presetTime}, 不能晚于过期时间")
                if (newTask.presetTime <= queuedTask.outDateTime) throw IllegalArgumentException("非法的任务预设时间${newTask.presetTime}, 不能早于已添加任务的过期时间")
            }

            taskMap[newTask.aliasComponentClassName] = newTask
        }
    }

    ...
}

LauncherIconManager.addNewTask(
    SwitchIconTask(
        SplashActivity::class.java.name,
        "$packageName.SplashAliasActivity",
        format.parse("2020-08-02").time,
        format.parse("2020-08-09").time
    ),
    SwitchIconTask(
        SplashActivity::class.java.name,
        "$packageName.SplashAlias2Activity",
        format.parse("2020-11-05").time,
        format.parse("2020-11-12").time
    )
)

The monitoring of the in-app Activity life cycle is registered through the Application#registerActivityLifecycleCallbacks method, and whether there is an active Activity to determine whether the application has entered the background:

/**
 * 应用运行状态注册器
 */
object RunningStateRegister {

    fun register(application: Application, callback: StateCallback) {
        application.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallbacks() {
            private var startedActivityCount = 0
            override fun onActivityStarted(activity: Activity) {
                if (startedActivityCount == 0) {
                    callback.onForeground()
                }
                startedActivityCount++
            }

            override fun onActivityStopped(activity: Activity) {
                startedActivityCount--
                if (startedActivityCount == 0) {
                    callback.onBackground()
                }
            }
        })
    }

}   

class BaseApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        LauncherIconManager.register(this)
    }
}

After judging that the application enters the background, you can start to replace the icon:

/**
 * 启动器图标管理器
 */
object LauncherIconManager {
    ...

    /**
     * 注册以监听应用运行状态
     */
    fun register(application: Application) {
        RunningStateRegister.register(application, object: RunningStateRegister.StateCallback{
            override fun onForeground() {
            }

            override fun onBackground() {
                proofreadingInOrder(application)
            }
        })
    }

    /**
     * 依次校对预设时间
     * @param context 上下文
     */
    fun proofreadingInOrder(context: Context) {
        for (task in taskMap.values) {
            if (proofreading(context, task)) break
        }
    }

    /**
     * 校对预设时间/过期时间
     * @param context 上下文
     * @return true 已过预设时间      false 未达预设时间或已过期
     */
    private fun proofreading(context: Context, task: SwitchIconTask) =
        when {
            isPassedOutDateTime(task) -> {
                disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
                enableComponent(context, task.launcherComponentClassName)
                false
            }
            isPassedPresetTime(task) -> {
                disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
                enableComponent(context, task.aliasComponentClassName)
                true
            }
            else -> false
        }

    /**
     * 是否已超过预设时间
     * @param task 任务
     */
    private fun isPassedPresetTime(task: SwitchIconTask) =
        System.currentTimeMillis() > task.presetTime

    /**
     * 是否已超过过期时间
     * @param task 任务
     *
     */
    private fun isPassedOutDateTime(task: SwitchIconTask) =
        System.currentTimeMillis() > task.outDateTime

    ...
}        

The above code has been uploaded to GitHub. The core classes are encapsulated in the Library module, and a Demo module is provided to demonstrate how to use it.


Effect preview


to sum up

Through the above-constructed solution, our APP can automatically replace the application icon at a preset time. The disadvantage is that it can only load image resources packaged with the APK, which is suitable for scenarios where the operation time is relatively fixed.

Guess you like

Origin blog.csdn.net/ajsliu1233/article/details/109336269