一起养成写作习惯!这是我参与「日新计划 4 月更文挑战」的第3天,点击查看活动详情。

前言

为了方便用户使用某些信息,我们有时会提供复制的功能。当这些复制的内容是用初始化电脑户的敏感信息时(例如姓名,身份证号或者银行卡号),我们希望可以在一段时间之后把这些信息从APP剪切板清除。

App或者系统重启这项任务也不能耽误,很容易联想到WorkManager。分享一个用WorkManager的实现,以及发现的一个小坑初始化失败是怎么解决

实现

简单地调用CLIPBOARD_SERVICE。注意:清除剪切板内容时,我们要检查最新的内容。如果还是当时的信息才进行清除。

// ClearClipboardWorker.kt
class ClearClipboardWorker(
    private val appContext: Context,
    private val workerParams: WorkerParameters
) : Worker(appContext, workerParams) {
    companion object {
        const val LABEL_PARAM = "label_param"
    }
    override fun doWork(): Result {
        val label = workerParams.inputData.getString(LABEL_PARAM)
        val clipboardManager =
            appContext.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
        return if (clipboardManager != null) {
            // 当剪切板上还是之前的内容才清除,否则不修改
            if (clipboardManager.primaryClip?.description?.label == label) {
                clipboardManager.setPrimaryClip(ClipData.newPlainText(label, ""))
            }
            Result.success()
        } else {
            Result.failure()
        }
    }
}

复制时,先把内容放到初始化sdk什么意思剪切板然后启动一个WorkManager

/**
 * 复制内容到剪切板并在一定时间后清除。
 *
 * @param context 用来获取 {@link ClipboardManager}
 * @param label   读取剪切板内容的标签
 * @param text    复制内容
 * @return 如果复制成功 <code>true</code>, 否则 <code>false</code>.
 */
public static boolean copyToClipboard(@NonNull Context context, @NonNull CharSequence label, @NonNull CharSequence text) {
    final ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
    if (clipboard == null) {
        return false;
    }
    ClipData clipData = ClipData.newPlainText(label, text);
    clipboard.setPrimaryClip(clipData);
    OneTimeWorkRequest request = createClearClipboardRequest(label.toString(), 1L, TimeUnit.HOURS);
    WorkManager.getInstance(context.getApplicationContext()).enqueue(request);
    return true;
}
public static OneTimeWorkRequest createClearClipboardRequest(String label, long delayDuration, TimeUnit delayTimeUnit) {
    return new OneTimeWorkRequest.Builder(ClearClipboardWorker.class)
            .setInitialDelay(delayDuration, delayTimeUnit)
            .setInputData(
                    new Data.Builder()
                            .putString(ClearClipboardWorker.LABEL_PARAM, label)
                            .build())
            .build();
}

这里有一个坑:上面的代码在API 29往上的版本只能在程序在前台时才生效(因为用户隐私的相关更新)。对线程是什么意思API 27及以下,需要剪切板已线程撕裂者初始化并且需要在main thread上才能做修改。

填坑

给核心代码套上用mainLooperHandler,这样内部的代码就会在主线程运行了:

class ClearClipboardWorker(
    private val appContext: Context,
    private val workerParams: WorkerParameters
) : Worker(appContext, workerParams) {
    companion object {
        const val LABEL_PARAM = "label_param"
    }
    override fun doWork(): Result {
        Handler(Looper.getMainLooper()).post {
            val label = workerParams.inputData.getString(LABEL_PARAM)
            val clipboardManager =
                appContext.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
            // 当剪切板上还是之前的内容才清除,否则不修改
            if (clipboardManager?.primaryClip?.description?.label == label) {
                clipboardManager?.setPrimaryClip(ClipData.newPlainText(label, ""))
            }
        }
        return Result.success()
    }
}

测试

可以通过写单元单元测试评语怎么写测试来验证将来加入的代码没有破坏原本的功能:

class ClearClipboardWorkerTest {
    @Before
    fun setUp() {
        val context: Context = ApplicationProvider.getApplicationContext()
        val config = Configuration.Builder()
            .setMinimumLoggingLevel(Log.DEBUG)
            .setExecutor(SynchronousExecutor())
            .build()
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
    }
    @Test
    fun shouldWipe_after_specifiedTime_ifPrimaryClipUnchanged() {
        val context: Context = ApplicationProvider.getApplicationContext()
        val request =
            DebitInstrumentUtils.createClearClipboardRequest("ID", 1, TimeUnit.HOURS)
        val workManager = WorkManager.getInstance(context.applicationContext)
        workManager.enqueue(request)
        val clipboardManager =
            context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        DebitInstrumentUtils.copyToClipboard(
            context,
            "ID",
            "32763498739025"
        )
        assertEquals("32763498739025", clipboardManager.primaryClip!!.getItemAt(0)!!.text)
        val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
        testDriver?.setInitialDelayMet(request.id)
        val workInfo = workManager.getWorkInfoById(request.id).get()
        assertEquals("", clipboardManager.primaryClip!!.getItemAt(0)!!.text)
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

总结线程池

使用W线程池orkManager来定时处理一些任务,有没有很方便呢?