Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const logBatteryInfo = async () => {
* [`getBatteryInfo()`](#getbatteryinfo)
* [`getLanguageCode()`](#getlanguagecode)
* [`getLanguageTag()`](#getlanguagetag)
* [`addListener('batteryChargingStateChange', ...)`](#addlistenerbatterychargingstatechange-)
* [`removeAllListeners()`](#removealllisteners)
* [Interfaces](#interfaces)
* [Type Aliases](#type-aliases)

Expand Down Expand Up @@ -119,6 +121,39 @@ Get the device's current language locale tag.
--------------------


### addListener('batteryChargingStateChange', ...)

```typescript
addListener(eventName: 'batteryChargingStateChange', listenerFunc: BatteryChargingStateChangeListener) => Promise<PluginListenerHandle>
```

Listen for changes to whether the device is charging (including when the battery becomes full while plugged in).

| Param | Type |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| **`eventName`** | <code>'batteryChargingStateChange'</code> |
| **`listenerFunc`** | <code><a href="#batterychargingstatechangelistener">BatteryChargingStateChangeListener</a></code> |

**Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>

**Since:** 8.1.0

--------------------


### removeAllListeners()

```typescript
removeAllListeners() => Promise<void>
```

Remove all listeners for this plugin.

**Since:** 8.1.0

--------------------


### Interfaces


Expand Down Expand Up @@ -168,11 +203,25 @@ Get the device's current language locale tag.
| **`value`** | <code>string</code> | Returns a well-formed IETF BCP 47 language tag. | 4.0.0 |


#### PluginListenerHandle

| Prop | Type |
| ------------ | ----------------------------------------- |
| **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |


### Type Aliases


#### OperatingSystem

<code>'ios' | 'android' | 'windows' | 'mac' | 'unknown'</code>


#### BatteryChargingStateChangeListener

Callback for battery charging state changes.

<code>(info: <a href="#batteryinfo">BatteryInfo</a>): void</code>

</docgen-api>
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.capacitorjs.plugins.device;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
Expand All @@ -11,11 +16,63 @@
@CapacitorPlugin(name = "Device")
public class DevicePlugin extends Plugin {

public static final String BATTERY_CHARGING_STATE_CHANGE_EVENT = "batteryChargingStateChange";

private Device implementation;
private Boolean lastBatteryChargingState;

private final BroadcastReceiver batteryStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
return;
}
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean charging =
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
if (lastBatteryChargingState == null) {
lastBatteryChargingState = charging;
return;
}
if (lastBatteryChargingState != charging) {
lastBatteryChargingState = charging;
notifyBatteryChargingStateChange(intent);
}
}
};

@Override
public void load() {
implementation = new Device(getContext());
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getContext().registerReceiver(batteryStateReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
getContext().registerReceiver(batteryStateReceiver, filter);
}
}

@Override
protected void handleOnDestroy() {
try {
getContext().unregisterReceiver(batteryStateReceiver);
} catch (IllegalArgumentException e) {
// Receiver was not registered
}
}

private void notifyBatteryChargingStateChange(Intent batteryIntent) {
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryLevel = level >= 0 && scale > 0 ? level / (float) scale : -1;
int status = batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging =
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;

JSObject data = new JSObject();
data.put("batteryLevel", batteryLevel);
data.put("isCharging", isCharging);
notifyListeners(BATTERY_CHARGING_STATE_CHANGE_EVENT, data);
}

@PluginMethod
Expand Down
40 changes: 38 additions & 2 deletions device/ios/Sources/DevicePlugin/DevicePlugin.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Foundation
import UIKit
import Capacitor

@objc(DevicePlugin)
public class DevicePlugin: CAPPlugin, CAPBridgedPlugin {
public static let batteryChargingStateChangeEvent = "batteryChargingStateChange"

public let identifier = "DevicePlugin"
public let jsName = "Device"
public let pluginMethods: [CAPPluginMethod] = [
Expand All @@ -13,6 +16,41 @@ public class DevicePlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "getLanguageTag", returnType: CAPPluginReturnPromise)
]
private let implementation = Device()
private var lastBatteryChargingState: Bool?

override public func load() {
UIDevice.current.isBatteryMonitoringEnabled = true
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryStateDidChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil
)
}

deinit {
NotificationCenter.default.removeObserver(self)
UIDevice.current.isBatteryMonitoringEnabled = false
}

@objc private func batteryStateDidChange() {
let state = UIDevice.current.batteryState
if state == .unknown {
return
}
let charging = state == .charging || state == .full
if lastBatteryChargingState == nil {
lastBatteryChargingState = charging
return
}
if lastBatteryChargingState != charging {
lastBatteryChargingState = charging
notifyListeners(DevicePlugin.batteryChargingStateChangeEvent, data: [
"batteryLevel": UIDevice.current.batteryLevel,
"isCharging": charging
])
}
}

@objc func getId(_ call: CAPPluginCall) {
if let uuid = UIDevice.current.identifierForVendor {
Expand Down Expand Up @@ -57,8 +95,6 @@ public class DevicePlugin: CAPPlugin, CAPBridgedPlugin {
"batteryLevel": UIDevice.current.batteryLevel,
"isCharging": UIDevice.current.batteryState == .charging || UIDevice.current.batteryState == .full
])

UIDevice.current.isBatteryMonitoringEnabled = false
}

@objc func getLanguageCode(_ call: CAPPluginCall) {
Expand Down
26 changes: 26 additions & 0 deletions device/src/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PluginListenerHandle } from '@capacitor/core';

export type OperatingSystem = 'ios' | 'android' | 'windows' | 'mac' | 'unknown';

export interface DeviceId {
Expand Down Expand Up @@ -123,6 +125,13 @@ export interface BatteryInfo {
isCharging?: boolean;
}

/**
* Callback for battery charging state changes.
*
* @since 8.1.0
*/
export type BatteryChargingStateChangeListener = (info: BatteryInfo) => void;

export interface GetLanguageCodeResult {
/**
* Two character language code.
Expand Down Expand Up @@ -176,4 +185,21 @@ export interface DevicePlugin {
* @since 4.0.0
*/
getLanguageTag(): Promise<LanguageTag>;

/**
* Listen for changes to whether the device is charging (including when the battery becomes full while plugged in).
*
* @since 8.1.0
*/
addListener(
eventName: 'batteryChargingStateChange',
listenerFunc: BatteryChargingStateChangeListener,
): Promise<PluginListenerHandle>;

/**
* Remove all listeners for this plugin.
*
* @since 8.1.0
*/
removeAllListeners(): Promise<void>;
}
52 changes: 52 additions & 0 deletions device/src/web.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { WebPlugin } from '@capacitor/core';

import type { PluginListenerHandle } from '@capacitor/core';

import type {
BatteryInfo,
BatteryChargingStateChangeListener,
DeviceId,
DeviceInfo,
DevicePlugin,
Expand All @@ -24,6 +27,55 @@ declare global {
}

export class DeviceWeb extends WebPlugin implements DevicePlugin {
private batteryApi: any = null;
private batteryListenersAttached = false;

private readonly handleBatteryChargingChange = (): void => {
if (!this.batteryApi) {
return;
}
this.notifyListeners('batteryChargingStateChange', {
batteryLevel: this.batteryApi.level,
isCharging: this.batteryApi.charging,
});
};

private async attachBatteryListeners(): Promise<void> {
if (this.batteryListenersAttached || typeof navigator === 'undefined' || !navigator.getBattery) {
return;
}
try {
this.batteryApi = await navigator.getBattery();
this.batteryApi.addEventListener('chargingchange', this.handleBatteryChargingChange);
this.batteryListenersAttached = true;
} catch (e) {
// Battery Status API unavailable or denied
}
}

private detachBatteryListeners(): void {
if (this.batteryApi && this.batteryListenersAttached) {
this.batteryApi.removeEventListener('chargingchange', this.handleBatteryChargingChange);
this.batteryListenersAttached = false;
this.batteryApi = null;
}
}

async addListener(
eventName: 'batteryChargingStateChange',
listenerFunc: BatteryChargingStateChangeListener,
): Promise<PluginListenerHandle> {
if (eventName === 'batteryChargingStateChange') {
void this.attachBatteryListeners();
}
return super.addListener(eventName, listenerFunc);
}

async removeAllListeners(): Promise<void> {
this.detachBatteryListeners();
return super.removeAllListeners();
}

async getId(): Promise<DeviceId> {
return {
identifier: this.getUid(),
Expand Down