/* * Copyright (c) linroid 2015. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.linroid.pushapp.service; import android.accessibilityservice.AccessibilityService; import android.annotation.TargetApi; import android.app.NotificationManager; import android.app.PendingIntent; import android.os.Build; import android.os.Build.VERSION; import android.os.Handler; import android.support.v4.app.NotificationCompat; import android.util.SparseArray; import android.view.KeyEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.linroid.pushapp.App; import com.linroid.pushapp.BuildConfig; import com.linroid.pushapp.Constants; import com.linroid.pushapp.R; import com.linroid.pushapp.model.Pack; import com.linroid.pushapp.util.AndroidUtil; import com.linroid.pushapp.util.BooleanPreference; import com.linroid.pushapp.util.DeviceUtil; import com.linroid.pushapp.util.IntentUtil; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import hugo.weaving.DebugLog; import timber.log.Timber; /** * Created by linroid on 7/27/15.<br/> * 免ROOT自动安装Apk<br/> * * See <a href="http://www.infoq.com/cn/articles/android-accessibility-installing">使用Android Accessibility实现免Root自动批量安装功能</a> */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public class ApkAutoInstallService extends AccessibilityService { public static final int AVAILABLE_API = 16; public static final int INSTALL_TIMEOUT = 60000; public static final int UNINSTALL_TIMEOUT = 5000; public static final int INSTALL_RETRY_INTERVAL = 1000; private static final String CLASS_NAME_APP_ALERT_DIALOG = "android.app.AlertDialog"; private static final String CLASS_NAME_LENOVO_SAFECENTER = "com.lenovo.safecenter"; private static final String CLASS_NAME_PACKAGE_INSTALLER = "com.android.packageinstaller"; private static final String CLASS_NAME_GOOGLE_PACKAGE_INSTALLER = "com.google.android.packageinstaller"; private static final String CLASS_NAME_PACKAGE_INSTALLER_ACTIVITY = "com.android.packageinstaller.PackageInstallerActivity"; private static final String CLASS_NAME_PACKAGE_INSTALLER_PERMSEDITOR = "com.android.packageinstaller.PackageInstallerPermsEditor"; private static final String CLASS_NAME_PACKAGE_INSTALLER_PROGRESS = "com.android.packageinstaller.InstallAppProgress"; private static final String CLASS_NAME_PACKAGE_UNINSTALLER_ACTIVITY = "com.android.packageinstaller.UninstallerActivity"; private static final String CLASS_NAME_PACKAGE_UNINSTALLER_PROGRESS = "com.android.packageinstaller.UninstallAppProgress"; private static final String CLASS_NAME_WIDGET_BUTTON = "android.widget.Button"; private static final String CLASS_NAME_WIDGET_LISTVIEW = "android.widget.ListView"; private static final String CLASS_NAME_WIDGET_TEXTVIEW = "android.widget.TextView"; private static boolean enable = false; // 需要先卸载再安装的应用 private static SparseArray<Pack> sPrepareList = new SparseArray<>(); private static SparseArray<Pack> sInstallList = new SparseArray<>(); private static SparseArray<Pack> sUninstallList = new SparseArray<>(); @Inject @Named(Constants.SP_AUTO_OPEN) public BooleanPreference autoOpen; @Inject NotificationManager notificationManager; Handler handler; Runnable handleInstallTimeout = new Runnable() { @Override public void run() { AccessibilityNodeInfo validInfo = getRootInActiveWindow(); if (validInfo != null) { if (autoOpen.getValue()) { boolean openSuccess = openAfterInstalled(null); Timber.d("成功打开?%s", openSuccess); } String label = validInfo.getText().toString(); removePackFromListByAppName(sInstallList, label, true); validInfo.recycle(); } } }; Runnable handleUninstallTimeout = new Runnable() { @Override public void run() { // 卸载超时,先尝试点击完成(即卸载成功),然后再看有没有待装的应用(即先卸载再安装的) AccessibilityNodeInfo eventInfo = getRootInActiveWindow(); if (eventInfo != null && sUninstallList != null && eventInfo.getText() != null) { String label = eventInfo.getText().toString(); removePackFromListByAppName(sUninstallList, label); // waring: 此处可能找不到 boolean success = performEventAction(eventInfo, getString(R.string.btn_accessibility_ok), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_know), false); eventInfo.recycle(); } processPrepareInstall(null); } }; /** * 安装应用 * * @param pack */ public static void addInstallPackage(Pack pack) { enable = true; if (pack != null) { sInstallList.put(pack.getId(), pack); } } /** * 卸载应用 * * @param pack */ public static void addUninstallApplication(Pack pack) { enable = true; if (pack != null) { sUninstallList.put(pack.getId(), pack); } } /** * 先卸载再安装的应用 * * @param pack */ public static void addPrepareInstallApplication(Pack pack) { enable = true; if (pack != null) { sUninstallList.put(pack.getId(), pack); sPrepareList.put(pack.getId(), pack); } } public static void reset() { enable = false; if (sInstallList != null) { sInstallList.clear(); } if (sUninstallList != null) { sUninstallList.clear(); } } public static boolean available() { return VERSION.SDK_INT >= AVAILABLE_API; } /** * 处理卸载后的安装 */ private void processPrepareInstall(String label) { Pack pack = null; if (sPrepareList.size() == 0) { return; } if (label != null) { for (int i = 0; i < sPrepareList.size(); i++) { int key = sPrepareList.keyAt(i); pack = sPrepareList.get(key); sPrepareList.remove(key); } } if (pack == null) { pack = sPrepareList.get(sPrepareList.size() - 1); sPrepareList.removeAt(sPrepareList.size() - 1); } addInstallPackage(pack); startActivity(IntentUtil.installApk(pack.getPath())); } @DebugLog @Override public void onCreate() { super.onCreate(); App.from(this).component().inject(this); handler = new Handler(); } @DebugLog @Override protected void onServiceConnected() { super.onServiceConnected(); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (enable && available()) { try { onProcessAccessibilityEvent(event); } catch (Exception e) { e.printStackTrace(); } } } @Override @DebugLog public void onInterrupt() { } @DebugLog @Override protected boolean onKeyEvent(KeyEvent event) { return true; } private void onProcessAccessibilityEvent(AccessibilityEvent event) { if (event.getSource() == null) { return; } String packageName = event.getPackageName().toString(); String className = event.getClassName().toString(); String sourceText = event.getSource().getText() == null ? BuildConfig.VERSION_NAME : event.getSource().getText().toString().trim(); if (packageName.equals(CLASS_NAME_PACKAGE_INSTALLER) || packageName.equals(CLASS_NAME_GOOGLE_PACKAGE_INSTALLER)) { if (isApplicationInstallEvent(event, className, sourceText)) { // 准备安装 onApplicationInstall(event); } else if (hasAccessibilityNodeInfoByText(event, getString(R.string.str_accessibility_install_blocked))) { // 准备安装 onApplicationInstall(event, false); } else if (isApplicationInstalledEvent(event, className, sourceText)) { // 安装完成 onApplicationInstalled(event); } else if (isApplicationInstallFailedEvent(event, className, sourceText)) { // 安装失败 onInstallFail(event); } else if (className.equalsIgnoreCase(CLASS_NAME_APP_ALERT_DIALOG)) { // 弹窗,如授权操作 processAlertDialogEvent(event, className, sourceText); } else if (isApplicationUninstallEvent(event, className, sourceText)) { // 准备卸载 onApplicationUninstall(event); } else if (isApplicationUninstalledEvent(event, className, sourceText)) { // 卸载完成 onApplicationUninstalled(event); } } else if (packageName.equals(CLASS_NAME_LENOVO_SAFECENTER)) { processLenovoEvent(event, className, sourceText); } } /** * 是否是准备安装事件 * * @param event * @param className * @param sourceText * @return */ private boolean isApplicationInstallEvent(AccessibilityEvent event, String className, String sourceText) { return className.equalsIgnoreCase(CLASS_NAME_PACKAGE_INSTALLER_ACTIVITY) || sourceText.contains(getString(R.string.btn_accessibility_install)); } /** * 是否是安装失败界面 * * @param event * @param className * @param sourceText * @return */ private boolean isApplicationInstallFailedEvent(AccessibilityEvent event, String className, String sourceText) { return className.equalsIgnoreCase(CLASS_NAME_WIDGET_TEXTVIEW) && (sourceText.contains(getString(R.string.str_accessibility_installed4)) || sourceText.contains(getString(R.string.str_accessibility_installed5))); } /** * 是否是安装完成事件 * * @param event * @param className * @param sourceText * @return */ private boolean isApplicationInstalledEvent(AccessibilityEvent event, String className, String sourceText) { return sourceText.equalsIgnoreCase(getString(R.string.btn_accessibility_open)) || sourceText.equalsIgnoreCase(getString(R.string.btn_accessibility_run)) || sourceText.contains(getString(R.string.str_accessibility_installed)) || sourceText.contains(getString(R.string.str_accessibility_installed2)) || sourceText.contains(getString(R.string.str_accessibility_installed3)) || hasAccessibilityNodeInfoByText(event, getString(R.string.str_accessibility_installed)) || hasAccessibilityNodeInfoByText(event, getString(R.string.str_accessibility_installed2)) || hasAccessibilityNodeInfoByText(event, getString(R.string.str_accessibility_installed3)); } /** * 是否是卸载事件 * * @param event * @param className * @param sourceText * @return */ private boolean isApplicationUninstallEvent(AccessibilityEvent event, String className, String sourceText) { return className.equalsIgnoreCase(CLASS_NAME_PACKAGE_UNINSTALLER_ACTIVITY) || sourceText.contains(getString(R.string.str_accessibility_uninstall)); } /** * 是否是卸载完成事件 * * @param event * @param className * @param sourceText * @return */ private boolean isApplicationUninstalledEvent(AccessibilityEvent event, String className, String sourceText) { return hasAccessibilityNodeInfoByText(event, R.string.str_accessibility_uninstalled) || hasAccessibilityNodeInfoByText(event, R.string.str_accessibility_uninstalled2) || hasAccessibilityNodeInfoByText(event, R.string.str_accessibility_uninstalled3); } /** * 弹窗,如安装失败、卸载等 * * @param event * @param className * @param sourceText */ private void processAlertDialogEvent(AccessibilityEvent event, String className, String sourceText) { Timber.d("出现弹窗"); String eventText = event.getText().toString(); if (eventText.contains(getString(R.string.str_accessibility_error))) { onInstallFail(event); } else if (!eventText.contains(getString(R.string.str_accessibility_uninstall))) { AccessibilityNodeInfo nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_ok)); if (nodeInfo != null) { performClick(nodeInfo); nodeInfo.recycle(); } } else if (eventText.contains(getString(R.string.str_accessibility_replace)) || eventText.contains(getString(R.string.str_accessibility_replace1))) { AccessibilityNodeInfo nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_ok)); if (nodeInfo != null) { performClick(nodeInfo); nodeInfo.recycle(); } } } private void processLenovoEvent(AccessibilityEvent event, String className, String sourceText) { Timber.d("联想"); if (sourceText.contains(getString(R.string.str_accessibility_installed3))) { onApplicationInstalled(event); } else if (sourceText.contains(getString(R.string.str_accessibility_uninstalled3))) { onApplicationUninstalled(event); } else { onApplicationInstall(event, CLASS_NAME_WIDGET_TEXTVIEW); } } /** * 安装失败 * * @param event */ private void onInstallFail(AccessibilityEvent event) { Timber.e("安装失败"); performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); // 从 sInstallList 转移到 prepare 的流程(先卸载再安装 if (sInstallList != null && sInstallList.size() > 0) { AccessibilityNodeInfo validInfo = getValidAccessibilityNodeInfo(event, sInstallList); if (validInfo != null && processApplicationUninstalled(event) && validInfo.getText() != null) { String label = validInfo.getText().toString(); removePackFromListByAppName(sInstallList, label); for (int i = 0; i < sInstallList.size(); i++) { int key = sInstallList.keyAt(i); Pack pack = sInstallList.get(key); if (pack.getAppName().equals(label)) { addPrepareInstallApplication(pack); sInstallList.remove(key); startActivity(IntentUtil.uninstallApp(pack.getPath())); break; } } validInfo.recycle(); } } } private void onApplicationInstall(AccessibilityEvent event) { onApplicationInstall(event, DeviceUtil.isFlyme() ? CLASS_NAME_WIDGET_TEXTVIEW : CLASS_NAME_WIDGET_BUTTON, true, false); } private void onApplicationInstall(AccessibilityEvent event, boolean maybeValidate) { onApplicationInstall(event, DeviceUtil.isFlyme() ? CLASS_NAME_WIDGET_TEXTVIEW : CLASS_NAME_WIDGET_BUTTON, maybeValidate, false); } private void onApplicationInstall(AccessibilityEvent event, String nodeClassName) { onApplicationInstall(event, nodeClassName, true, false); } /** * 准备安装 * * @param event Accessibility 捕获到的事件 * @param nodeClassName 需要遍历的节点是什么类型的 * @param maybeValidate 是否需要校验 * @param isRetry 是否是重试 */ private void onApplicationInstall(final AccessibilityEvent event, final String nodeClassName, final boolean maybeValidate, boolean isRetry) { Timber.d("准备安装"); // 设置安装超时(即最后未捕获到安装失败的话) handler.postDelayed(handleInstallTimeout, INSTALL_TIMEOUT); if (!maybeValidate || isValidPackageEvent(event, sInstallList)) { AccessibilityNodeInfo nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_install)); if (nodeInfo != null) { performClick(nodeInfo); nodeInfo.recycle(); return; } nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_allow_once)); if (nodeInfo != null) { performClick(nodeInfo); nodeInfo.recycle(); return; } nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_next)); if (nodeInfo != null) { performClick(nodeInfo); onApplicationInstall(event); nodeInfo.recycle(); } nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.str_accessibility_replace_continue)); if (nodeInfo != null) { performClick(nodeInfo); onApplicationInstall(event); nodeInfo.recycle(); } // 低端机型,页面还没加载出来,事件却已经先发到了,故加入延迟机制 if (nodeInfo == null && !isRetry) { handler.postDelayed(new Runnable() { @Override public void run() { onApplicationInstall(event, nodeClassName,maybeValidate, true); } }, INSTALL_RETRY_INTERVAL); } } } /** * 安装完成 * * @param event */ private void onApplicationInstalled(AccessibilityEvent event) { Timber.d("安装完成"); // 取消安装超时的操作 handler.removeCallbacks(handleInstallTimeout); AccessibilityNodeInfo validInfo = getValidAccessibilityNodeInfo(event, sInstallList); if (validInfo != null) { if (autoOpen.getValue()) { boolean openSuccess = openAfterInstalled(event); Timber.d("成功打开?%s", openSuccess); } String label = validInfo.getText().toString(); removePackFromListByAppName(sInstallList, label, true); validInfo.recycle(); } } private void removePackFromListByAppName(SparseArray<Pack> sInstallList, String label) { removePackFromListByAppName(sInstallList, label, false); } /** * 通过应用名称从列表中移除 * * @param list 保存的列表 * @param appName 应用名称 * @param becauseInstalled 是因为安装完成了才移除的 */ private void removePackFromListByAppName(SparseArray<Pack> list, String appName, boolean becauseInstalled) { for (int i = 0; i < list.size(); i++) { int key = list.keyAt(i); Pack pack = list.get(key); if (pack.getAppName().equals(appName)) { sInstallList.remove(key); if (becauseInstalled) { showInstalledNotification(pack); } break; } } if (sInstallList.size() == 0 && sUninstallList.size() == 0) { enable = false; } } /** * 显示安装完成的通知 * * @param pack */ private void showInstalledNotification(Pack pack) { PendingIntent intent = PendingIntent.getActivity(this, 0, AndroidUtil.getOpenAppIntent(this, pack.getPackageName()), 0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setContentTitle(getString(R.string.msg_download_title, pack.getAppName(), pack.getVersionName())) .setSmallIcon(R.drawable.ic_stat_complete) .setAutoCancel(true) .setContentIntent(intent) .setContentText(getString(R.string.msg_install_complete)); AndroidUtil.openApplication(this, pack.getPackageName()); notificationManager.notify(pack.getId(), builder.build()); } /** * 安装完成之后打开应用 * * @param event * @return */ private boolean openAfterInstalled(AccessibilityEvent event) { AccessibilityNodeInfo eventInfo; if (event != null && event.getSource() != null) { eventInfo = event.getSource(); } else { eventInfo = getRootInActiveWindow(); } boolean success = false; if (eventInfo != null) { success = performEventAction(eventInfo, getString(R.string.btn_accessibility_run), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_open), false); eventInfo.recycle(); } return success; } /** * 安装完成之后关闭安装窗口 * * @param event * @return */ private boolean closeAfterInstalled(AccessibilityEvent event) { performGlobalAction(GLOBAL_ACTION_BACK); return true; /*AccessibilityNodeInfo eventInfo; if (event != null && event.getSource() != null) { eventInfo = event.getSource(); } else { eventInfo = getRootInActiveWindow(); } boolean success = false; if (eventInfo != null) { success = performEventAction(eventInfo, getString(R.string.btn_accessibility_ok), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_done), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_complete), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_know), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_know), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_run), true) || performEventAction(eventInfo, getString(R.string.btn_accessibility_open), true); eventInfo.recycle(); } return success;*/ } /** * 准备卸载 * * @param event */ private void onApplicationUninstall(AccessibilityEvent event) { Timber.d("准备卸载"); // 设置卸载超时(即最后未捕获到卸载失败的话) handler.postDelayed(handleUninstallTimeout, UNINSTALL_TIMEOUT); if (isValidPackageEvent(event, sUninstallList)) { AccessibilityNodeInfo nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_uninstall)); if (nodeInfo != null) { performClick(nodeInfo); return; } nodeInfo = getAccessibilityNodeInfoByText(event, getString(R.string.btn_accessibility_ok)); if (nodeInfo != null) { performClick(nodeInfo); nodeInfo.recycle(); } } } /** * 卸载完成 * * @param event */ private void onApplicationUninstalled(AccessibilityEvent event) { Timber.d("卸载完成"); // 取消卸载超时的操作 handler.removeCallbacks(handleUninstallTimeout); AccessibilityNodeInfo validInfo = getValidAccessibilityNodeInfo(event, sUninstallList); String label = null; if (validInfo != null && processApplicationUninstalled(event) && sUninstallList != null && validInfo.getText() != null) { label = validInfo.getText().toString(); removePackFromListByAppName(sUninstallList, label); validInfo.recycle(); } // 如果卸载完成是为了安装某个Apk,则执行安装 processPrepareInstall(label); } /** * 处理卸载应用 * * @param event * @return */ private boolean processApplicationUninstalled(AccessibilityEvent event) { performGlobalAction(GLOBAL_ACTION_BACK); return true; /*AccessibilityNodeInfo eventInfo = null; if (event != null && event.getSource() != null) { eventInfo = event.getSource(); } else { eventInfo = getRootInActiveWindow(); } boolean success = false; if (eventInfo != null) { success = performEventAction(eventInfo, getString(R.string.btn_accessibility_ok), false) || performEventAction(eventInfo, getString(R.string.btn_accessibility_know), false); eventInfo.recycle(); } return success;*/ } private boolean hasAccessibilityNodeInfoByText(AccessibilityEvent event, int resId) { return hasAccessibilityNodeInfoByText(event, getString(resId)); } /** * 通过文本判断是否存在AccessibilityNodeInfo * * @param event * @param text * @return */ private boolean hasAccessibilityNodeInfoByText(AccessibilityEvent event, String text) { List<AccessibilityNodeInfo> nodes = null; if (event != null && event.getSource() != null) { nodes = event.getSource().findAccessibilityNodeInfosByText(text); } else { AccessibilityNodeInfo info = getRootInActiveWindow(); if (info != null) { nodes = info.findAccessibilityNodeInfosByText(text); } } return !(nodes == null || nodes.size() <= 0); } /** * 判断是否是指定包的事件 * * @param event * @param validPackageList * @return */ private boolean isValidPackageEvent(AccessibilityEvent event, SparseArray<Pack> validPackageList) { return getValidAccessibilityNodeInfo(event, validPackageList) != null; } /** * 获取有效的的AccessibilityNodeInfo * * @param event * @param validPackageList 包列表 * @return */ private AccessibilityNodeInfo getValidAccessibilityNodeInfo(AccessibilityEvent event, SparseArray<Pack> validPackageList) { if (validPackageList != null && validPackageList.size() > 0) { for (int i = 0; i < validPackageList.size(); i++) { int key = validPackageList.keyAt(i); Pack pack = validPackageList.get(key); AccessibilityNodeInfo nodeInfo = getAccessibilityNodeInfoByText(event, pack.getAppName()); if (nodeInfo != null) { return nodeInfo; } } } return null; } /** * 通过文本获取AccessibilityNodeInfo 对象 * * @param event * @param text * @return */ private AccessibilityNodeInfo getAccessibilityNodeInfoByText(AccessibilityEvent event, String text) { List<AccessibilityNodeInfo> nodes = null; // 加入try-catch防崩溃,因为安装时的延迟调用可能会导致 event.getSource 产生奇妙的 NullPointerException try { if (event != null && event.getSource() != null) { nodes = event.getSource().findAccessibilityNodeInfosByText(text); } } catch (Exception e) { } // 不用else,而是主动判断,提高识别的成功率 if (nodes == null || nodes.size() == 0){ AccessibilityNodeInfo info = getRootInActiveWindow(); if (info != null) { nodes = info.findAccessibilityNodeInfosByText(text); } } if (nodes != null && nodes.size() > 0) { for (AccessibilityNodeInfo nodeInfo : nodes) { String nodeText = nodeInfo.getText() == null ? BuildConfig.VERSION_NAME : nodeInfo.getText().toString(); //nodeInfo.getClassName().equals(className) && if (nodeText.equalsIgnoreCase(text)) { return nodeInfo; } nodeInfo.recycle(); } } return null; } /** * 进行动作事件操作 * * @param info * @param text * @param isGlobalAction 是否是全局动作 * @return 是否执行成功 */ private boolean performEventAction(AccessibilityNodeInfo info, String text, boolean isGlobalAction) { if (info == null) { return false; } List<AccessibilityNodeInfo> nodes = info.findAccessibilityNodeInfosByText(text); if (nodes != null && nodes.size() > 0) { for (AccessibilityNodeInfo nodeInfo : nodes) { String nodeText = nodeInfo.getText() == null ? null : nodeInfo.getText().toString(); if (text.equalsIgnoreCase(nodeText)) { if (isGlobalAction) { //返回 performGlobalAction(GLOBAL_ACTION_BACK); } else { performClick(nodeInfo); } return true; } nodeInfo.recycle(); } } return false; } /** * 进行点击操作 * * @param node * @return 是否点击成功 */ private boolean performClick(AccessibilityNodeInfo node) { return node != null && node.isEnabled() && node.isClickable() && node.performAction(AccessibilityNodeInfo.ACTION_CLICK); } }