构建一个无法关闭的安卓 POS 应用
已发表: 2022-03-11移动应用程序开发的世界是广阔且不断发展的,几乎每天都会出现新的框架和技术。 当您想到移动设备时,您可能会想到您的手机或平板电脑,尽管它们远没有智能手机那么受欢迎。
苹果的 iOS 和谷歌的安卓在移动市场上占据主导地位,在过去的十年里它们都有起起落落。 今天,我将更多地讨论 Android 及其在不一定是移动设备上的使用。
开源对谷歌的移动操作系统产生了非常有趣的副作用。 当然,我们可能会想到来自不同智能手机公司的所有不同的 Android 分支,但是所有运行 Android 的非移动设备呢? 如今,从冰箱、智能烤箱、门锁甚至销售点 (POS) 设备等各种设备都可以运行 Android。 后者是我最终写这篇文章的原因。
安卓 POS 系统
大约一年前,我开始使用一款非常普通的 Android 设备,而且大多数人都不太可能使用它。 有问题的设备是来自一家中国供应商的基于 Android 的 POS 系统,该系统还具有集成的热敏打印机(例如用于在商店或 ATM 上打印收据的打印机)。
不过,最大的惊喜是它的软件:它运行的是安卓的骨干版本。 如果我没记错的话,当时它运行的是 Android 8,或者如果你更喜欢 Google 代号,它运行的是 Android Oreo。 该设备本身看起来像一个老式的便携式 POS 设备,但它不是用于输入 PIN 的物理键盘,而是像过去的 Android 手机中使用的那样采用电容式触摸屏。
我的要求很简单:我必须看看是否有一种方法可以在运行我们正在开发的应用程序的同时使用该设备的功能,例如热敏打印机。 当我意识到需求本身是可能的时,另一个问题引起了我的注意:安全性。
问题是,如果你有一个处理卡支付和其他类型交易的设备,你可能不希望同一设备能够运行 TikTok、Gmail 或 Snapchat。 该设备的行为与平板电脑完全一样,它甚至还预装了 Google 的 Play 商店。 想象一下去一家小型便利店,看到你的收银员在自拍,打开尼日利亚王子的电子邮件,浏览奇怪的、充满恶意软件的网站。
之后,收银员会递给您相同的设备以输入您的 PIN 码。 就个人而言,通过这样的设备提供我的信用卡信息我会感到不安全。
将用户锁定在 Android 菜单之外
抛开安全不谈,我不得不接受一个更重要的挑战:我必须将使用 Android POS 设备的人锁定在我的应用程序中。 由于这些设备是交付给非技术人员的,因此无法选择使用操作系统。
当然,收银员不仅能够安装应用程序,但他们中的大多数人无法刷新自定义 ROM 或处理其他较低级别的操作。 该应用程序本身是用 React Native 编写的,尽管在这种情况下这无关紧要。 我所做的所有修改都是在本机 Java 代码中进行的,因此无论您使用什么来开发主应用程序,这些调整都应该有效。
作为一点免责声明,此过程仅适用于 Android 应用程序。 Apple 没有给我们在 iPhone 或 iPad 上轻松完成此类事情所需的控制权,鉴于 iOS 的封闭性,这是可以理解的。
用户可以通过四种方式退出应用程序:
- 使用主页按钮。
- 使用返回按钮。
- 使用“最近”按钮。
- 通过通知栏离开您的应用程序。
单击最近的通知或从该栏转到设置都会导致用户退出我们的应用程序。 您也有手势,但归根结底,这些手势会触发与常规按钮按下完全相同的动作。
此外,拥有解锁应用程序的 PIN 系统对于管理设备的人非常有用。 这样,只有持有 PIN 码的人才能安装不同版本的应用程序,而无需为最终用户提供更深入的访问权限。
主页按钮
为了防止用户按下 Home 按钮,我们不必实际禁用它。
Android 的一项有用功能是提供不同的启动器。 通常,这些应用程序为您提供不同的主屏幕、应用程序抽屉以及对各种 UI 自定义的访问。 每台 Android 设备都由制造商预装了一个。 最终,这些只是普通的常规应用程序,只有一个小但至关重要的例外。
这意味着如果操作系统可以将我们的应用程序识别为启动器,我们可以将其设置为默认启动器。 这样做的副作用是,每次您按下 Home 按钮时,设备都会将您带到 Home 启动器。 如果我们的应用程序是 Home 启动器,那么基本上,这个 Home 按钮就变得无用了。 为此,我们必须在我们的 Android 项目中编辑 AndroidManifest XML 文件并添加这两行代码:
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.HOME" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
第一行将使我们的应用程序有资格在用户按下 Home 按钮时被选中,第二行允许我们的应用程序在此操作发生时被设置为默认值。
现在,唯一剩下要做的就是让现场代理在将应用程序交付给客户时在设备上安装应用程序。 每当您安装可能成为启动器的应用程序时,Android 都会询问您是否要使用另一个启动器以及是否要将其设置为默认启动器。
所以现在,如果您按下主页按钮或清除所有最近的应用程序,设备会自动将您定向到我的应用程序。
后退按钮
接下来,我们必须处理后退按钮。 移动应用程序通常提供通过屏幕返回的屏幕方式,特别是因为许多设备没有专用的“返回”键。
几年前,Apple 的 iOS 设备就是这种情况,它采用了已经标志性的设计,屏幕下方只有一个物理按钮。 然而,近年来,大多数 Android 设备也放弃了物理 Home 按钮。 首先,他们转向屏幕按钮,现在我们看到他们正在逐步淘汰手机,取而代之的是手势,因为手机制造商转向具有小边框和下巴的全屏设备。
这意味着 Android 默认提供的后退按钮并不是真正需要的,为了使这个按钮完全无用,我们只需要在我们的活动中添加一个简单的代码块:
@Override public void onBackPressed() { }
这是一段非常简单的代码:我们的主要活动允许我们在用户按下后退按钮时进行拦截。 在我们的例子中,由于我们不希望用户按下该按钮太多次以退出应用程序,我们可以简单地用一个什么都不做的方法覆盖默认方法,告诉我们的应用程序在返回的情况下什么也不做按钮被按下。

这就是某些应用程序在您通过返回太多次而意外退出它们之前要求确认的方式。
最近按钮
我们仍然需要处理“最近”按钮,这是最棘手的一个。 此外,这绝对不是最佳实践,也不是您应该推送到 Play 商店的东西,但它确实适用于我们这里的小众案例。
与主活动让我们知道何时按下后退按钮的方式相同,它也让我们知道应用程序何时暂停。 这是什么意思? 每当我们的应用程序从前台应用程序切换到后台时,都会触发此代码。
在拦截这个事件的时候,我们会得到我们当前应用的任务ID,并告诉活动管理器把这个任务移到最前面。 为此,我们需要在之前编辑的同一个 Android 清单文件中获得一项特殊权限。
<manifest xmlns:andro package="com.johnwick"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.REORDER_TASKS" /> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.HOME" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> </application> </manifest>
这将允许我们阅读正在进行的任务并对这些任务进行更改。 此外,我们仍然需要截取应用程序被发送到后台的时刻。 我们可以再次覆盖活动中的onPause
方法。
在这里,我们获取任务管理器并强制它将特定任务移动到前台。 在我们的例子中,该特定任务是刚刚发送到后台(我们的应用程序)的任务。
@Override public void onPause() { super.onPause(); ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE); activityManager.moveTaskToFront(getTaskId(), 0); }
现在,每次您想进入最近的菜单时,应用程序都会自动重新聚焦。 当然,您有时可能会出现一点屏幕闪烁,但您将无法退出此应用程序。 还有一件更酷的事情 - 还记得我说过您也可以通过单击通知或通过通知托盘直接进入设置来退出吗? 好吧,执行这些操作会将应用程序置于后台,这将触发我们的代码,然后用户会立即被推回。
所有这些都发生得如此之快,以至于用户不会注意到后台发生的事情。 此外,这种方法的另一个好处是您的快速切换仍然可用。 例如,您仍然可以选择 wifi 网络,或禁用声音,但任何需要您进入实际设置应用程序的操作都是不允许的。
解决方案
我不确定这是不是最好的方法,但在研究一个我什至不知道可能的主题时,这仍然是一个非常有趣的过程。 它有效! 一个警告:此时,作为开发人员退出应用程序的方式只有两种——要么重新安装操作系统,要么通过 ADB 杀死/卸载应用程序。
如果您不知何故失去了与设备的 ADB 连接,我不知道有什么简单的方法可以让您离开。 为了避免这种情况,我最终建立了一个 PIN 系统。
边缘案例
有几种情况我们需要确保我们考虑到这一点。 首先,如果设备重启怎么办? 它不一定是手动重启,也可能是操作系统崩溃。
由于我们之前将我们的应用程序设置为默认启动器,因此一旦操作系统启动备份,它应该会自动启动我们的应用程序。 但是 Android 是如何知道在启动时加载主屏幕的呢? 这是因为它基本上只是加载您的默认启动器。 由于此时我们是默认启动器,因此重新启动应该不是问题。 Android 会在某个时候扼杀我们的应用程序吗? 从理论上讲,如果 RAM 内存填满,它可能会杀死应用程序,但在现实生活中,这几乎是不可能的。 由于我们的应用程序是不可关闭的,没有人可以打开其他应用程序,因此 RAM 内存不应该填满。
我能想到填充它的唯一方法是如果我们的应用程序有巨大的内存泄漏,但在这种情况下,你会遇到比将用户留在应用程序中更大的问题。 尽管如此,即使 Android 以某种方式向我们的应用程序触发了终止信号,每当您尝试回家时,操作系统都会尝试再次启动我们的应用程序,因为它是默认启动器,从而使用户锁定。
建立后门
作为快速解释,应用程序设置中有一个地方可以输入 PIN 码来解锁应用程序。 如果 PIN 正确,它将通过执行一个简单的条件语句来禁用我们的 onPause 和 onBackPressed 方法设置的限制。 从那里,用户将被允许通过快速切换菜单输入设置。 之后,您始终可以将默认启动器设置回库存启动器,这将使您完全退出应用程序。 有很多方法可以处理这部分,但最好有一种机制来禁用您设置的相同限制。 也许你可以做一个指纹认证来解锁。 可能性几乎是无穷无尽的。
包起来
最终,我留下了一个没有人可以关闭或杀死的应用程序。 即使重新启动设备也无济于事,因为它会直接重新启动到默认启动器,当前是我们的应用程序。 事实证明它对我们的项目很有用,尝试如此古怪和不合时宜的东西的满足感确实很棒,而且非常激励人心。
在许多设备和用例中,Android 让开发人员的生活变得轻松。 如今,编写 Android 应用程序比使用许多不同的特定于平台的语言和工具要容易得多。 想想物联网设备、信息亭应用程序、销售点系统、出租车的导航和支付网关等等。
这些是 Android 使应用程序开发更容易的用例,但也是您希望以与我们在本文中演示的方式类似的方式限制访问的利基用例。