使用 NativeScript 的 Android 持续后台服务

Posted 小陈乱敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 NativeScript 的 Android 持续后台服务相关的知识,希望对你有一定的参考价值。

最近,我开始着手在 android 上制作专门的语音助手。至少可以说我与 Java 关系密切,而且我还没有时间玩 Kotlin,NativeScript 似乎是显而易见的选择。

现在这是一项正在进行的工作,但我已经了解了很多关于 Android 的知识,我想与您分享一些我的发现。

首先,对于这项任务,我需要不断地听唤醒词并做出相应的反应。在任何平台上实现此功能的明显选择是某种后台服务或守护程序。

当谷歌搜索 nativescript 和后台服务时,一个优秀的教程和一个示例 repo 出现在顶部(我正在谈论这个)。

唉,这是使用 IntentService ,它只按计划运行并在任务完成后退出。
虽然创建一个连续的后台服务非常容易,但缺少关于这个主题的示例(本文旨在解决这个问题)。

设置

对于本文,我假设我们正在使用
typescript hello_world 模板:

tns create ServiceExample --ts --appid tk.ozymandias.ServiceExample

适应其他模板/技术应该不难。

服务

首先在 下新建一个子文件夹app/,我们调用它service。这纯粹是为了保持您的项目结构干净整洁。现在使用这些内容
创建一个新文件app/service/continuous_service.android.ts

export const CONTINUOUS_SERVICE_CLASSNAME = "tk.ozymandias.ServiceExample.Continuous_Service";


@JavaProxy("tk.ozymandias.ServiceExample.Continuous_Service")
class Continuous_Service extends android.app.Service 
    private timerId: number;

    onBind(): android.os.IBinder 
        return null;
    

    onCreate(): void 
        super.onCreate();
        console.log("SERVICE CREATED");

        if (!this.timerId) 
            this.timerId = setInterval(() => 
                console.log("PING");
            , 1000)
        
    

    onStartCommand(intent: android.content.Intent, flags: number, startId: number): number 
        console.log("SERVICE STARTED");
        return android.app.Service.START_REDELIVER_INTENT;
    

    onDestroy(): void 
        console.log("SERVICE DESTROYED");
        super.onDestroy();
        clearInterval(this.timerId);
    

现在这是一个非常基本的服务,它只是在后台运行并每秒向控制台打印“PING”。

在顶部,我们将服务名称导出为常量,稍后将在几个地方使用它。
唉,您至少需要在另外两个地方将服务名称指定为字符串文字。

第一个在这里很明显:@JavaProxy注释。
在此处使用变量将引发关于现有扩展的错误,而不是未定义的变量值。

第二个将在清单中。稍后再谈。

onCreate实例化服务时调用一次,onStartCommand每次服务启动onDestroy时调用,服务退出时调用。

服务如何启动和重新启动取决于
您从onStartCommand. 您可能很想回到START_STICKY这里,但这会在您的应用程序被终止时导致崩溃,因为系统会尝试重新启动您的服务null。

使其连续

到目前为止,我们有一个从您的应用程序开始的功能服务!但是当应用程序退出或被杀死时,我们如何让它继续运行呢?

让我们从制作广播接收器开始。

import  CONTINUOUS_SERVICE_CLASSNAME  from "./continuous-service.android";


export const RESTART_RECEIVER_CLASSNAME = "tk.ozymandias.ServiceExample.Restart_Receiver";


@JavaProxy("tk.ozymandias.ServiceExample.Restart_Receiver")
class Restart_Receiver extends android.content.BroadcastReceiver 
    onReceive(context: android.content.Context, intent: android.content.Intent): void 
        console.log("RESTART INTENT RECEIVED");
        const serviceIntent = new android.content.Intent();
        serviceIntent.setClassName(context, CONTINUOUS_SERVICE_CLASSNAME);
        context.startService(serviceIntent);
    

然后让我们稍微修改一下我们的服务以在退出时调用广播接收器,以便它可以重新启动我们的服务。

// At the top
import  RESTART_RECEIVER_CLASSNAME  from "./restart-receiver.android";

// In the onDestroy method in our service
    onDestroy(): void 
        // ...
        const restartIntent = new android.content.Intent();
        restartIntent.setClassName(this, RESTART_RECEIVER_CLASSNAME);
        this.sendBroadcast(restartIntent);
    

您还应该onTaskRemoved在我们的服务中实现方法。
当用户从最近视图中滑动您的应用程序时调用它。
在这种情况下(可能还有其他情况)onDestroy,默认情况下不会调用。
所以让我们通过调用onDestroy来调用stopSelf!

// ...
    onTaskRemoved(intent: android.content.Intent): void 
        console.log("TASK REMOVED");
        this.stopSelf();
    

现在我们有一个持续运行的服务!当应用程序退出或被杀死时,我们调用广播接收器,
然后重新启动我们的服务。

不幸的是,在较新版本的 Android 中,当系统
由于内存不足或电池优化而终止您的应用程序时,onDestroy不能保证会被调用。

前台服务

幸运的是,有一种官方的方法可以解决这个问题。
我们需要的是让我们的服务成为前台服务。
缺点是我们必须提供持久通知,但是从 Oreo 开始,此通知可以从系统设置中隐藏,
而不会影响我们的服务。

我们需要再次修改我们的服务,这次是
onCreate方法:

// In the onCreate method in our service
    onCreate(): void 
        // ...
        const builder: android.app.Notification.Builder = new android.app.Notification.Builder(app.android.context);
        // Need to check api level, NotificationChannel is required but only available on Oreo and above
        if (android.os.Build.VERSION.SDK_INT >= 26) 
            const channel: android.app.NotificationChannel = new android.app.NotificationChannel(
                "persistence", "Service running indicator", android.app.NotificationManager.IMPORTANCE_LOW
            );
            const manager: android.app.NotificationManager = (<android.app.Activity>app.android.context).getSystemService(android.content.Context.NOTIFICATION_SERVICE);
            channel.enableLights(false);
            channel.enableVibration(false);
            manager.createNotificationChannel(channel);
            builder.setChannelId("persistence");
        
        const notification: android.app.Notification = builder.build();
        this.startForeground(13, notification);
    

这使得持续的前台服务具有
持久的通知,
无论如何都会保持运行(它仍然可以从设置中强制停止)。

收尾工作

现在,如果您尝试到目前为止的代码,它将崩溃。
那是因为我们没有在
AndroidManifest.xml!
我们需要声明的是我们需要的权限(仅在最新版本的 Android 上)、服务和接收者。

事不宜迟,这是清单:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="__PACKAGE__"
    android:versionCode="1"
    android:versionName="1.0">

    <supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"/>

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:name="com.tns.NativeScriptApplication"
        android:allowBackup="true"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

        <activity
            android:name="com.tns.NativeScriptActivity"
            android:label="@string/title_activity_kimera"
            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode"
            android:theme="@style/LaunchScreenTheme">

            <meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.tns.ErrorReportActivity"/>
        <service android:enabled="true" android:name="tk.ozymandias.ServiceExample.Continuous_Service" />
        <receiver
            android:name="tk.ozymandias.ServiceExample.Restart_Receiver"
            android:enabled="true"
            android:exported="true"
            android:label="ContinuousServiceRestarter" />
    </application>
</manifest>

小彩蛋

您可能已经注意到,我们收到的通知是
通用的“应用程序正在运行”通知,点击时会转到设置。
我们可以做得更好!

// In the onCreate method in our service
    onCreate(): void 
        // ...
        const appIntent: android.content.Intent = new android.content.Intent(app.android.context, com.tns.NativeScriptActivity.class);
        const pendingIntent: android.app.PendingIntent = android.app.PendingIntent.getActivity(app.android.context, 0, appIntent, 0);
        const builder: android.app.Notification.Builder = new android.app.Notification.Builder(app.android.context);
        builder
            .setContentText("Custom notification, F'Yeah!")
            .setSmallIcon(android.R.drawable.btn_star_big_on)
            .setContentIntent(pendingIntent);
        // ...
    

以上是关于使用 NativeScript 的 Android 持续后台服务的主要内容,如果未能解决你的问题,请参考以下文章

Nativescript - 在 Android 中隐藏状态栏

Nativescript android删除操作栏

NativeScript 使用 Vue 获取当前的 android 上下文?

在 Android 上运行 nativescript 应用程序时出错

使用 NativeScript 的 Android 持续后台服务

尝试在 android (Nativescript-vue) 上使用 RadListView 时出现 Webpack 错误