Skip to content

Commit

Permalink
add PowerModeModule and optimized thermal status event reading
Browse files Browse the repository at this point in the history
  • Loading branch information
kristian-mkd committed Nov 20, 2024
1 parent 1d474a1 commit b6b7e9b
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class MainApplication : Application(), ReactApplication {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(VideoEffectsPackage())
add(ThermalPackage())
add(PowerModePackage())
}

override fun getJSMainModuleName(): String = "index"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.getstream.rnvideosample

import android.provider.Settings
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod

class PowerModeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String = "PowerModeModule"

@ReactMethod
fun isLowPowerModeEnabled(promise: Promise) {
try {
val lowPowerMode = Settings.Global.getInt(
reactContext.contentResolver,
"low_power"
)
promise.resolve(lowPowerMode == 1)
} catch (e: Settings.SettingNotFoundException) {
promise.reject("ERROR", e.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.getstream.rnvideosample

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class PowerModePackage : ReactPackage {
override fun createViewManagers(reactContext: ReactApplicationContext) = emptyList<ViewManager<*, *>>()

override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(PowerModeModule(reactContext))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,69 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.modules.core.DeviceEventManagerModule

class ThermalModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String = "ThermalModule"

private var thermalStatusListener: PowerManager.OnThermalStatusChangedListener? = null

@ReactMethod
fun startThermalStatusUpdates(promise: Promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val powerManager = reactContext.getSystemService(ReactApplicationContext.POWER_SERVICE) as PowerManager

val listener = PowerManager.OnThermalStatusChangedListener { status ->
val thermalStatus = when (status) {
PowerManager.THERMAL_STATUS_NONE -> "NONE"
PowerManager.THERMAL_STATUS_LIGHT -> "LIGHT"
PowerManager.THERMAL_STATUS_MODERATE -> "MODERATE"
PowerManager.THERMAL_STATUS_SEVERE -> "SEVERE"
PowerManager.THERMAL_STATUS_CRITICAL -> "CRITICAL"
PowerManager.THERMAL_STATUS_EMERGENCY -> "EMERGENCY"
PowerManager.THERMAL_STATUS_SHUTDOWN -> "SHUTDOWN"
else -> "UNKNOWN"
}

reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onThermalStatusChanged", thermalStatus)
}

thermalStatusListener = listener
powerManager.addThermalStatusListener(listener)
// Get initial status
getCurrentThermalStatus(promise)
} else {
promise.resolve("NOT_SUPPORTED")
}
} catch (e: Exception) {
promise.reject("THERMAL_ERROR", e.message)
}
}

@ReactMethod
fun stopThermalStatusUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val powerManager = reactContext.getSystemService(ReactApplicationContext.POWER_SERVICE) as PowerManager
// Store the current listener in a local val for safe null checking
val currentListener = thermalStatusListener
if (currentListener != null) {
powerManager.removeThermalStatusListener(currentListener)
thermalStatusListener = null
}
}
}

override fun getConstants(): Map<String, Any> {
return mapOf(
"THERMAL_EVENT" to "onThermalStatusChanged"
)
}

// TODO: remove if this is not needed
@ReactMethod
fun getCurrentThermalStatus(promise: Promise) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import {
Lobby,
useI18n,
} from '@stream-io/video-react-native-sdk';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Pressable, StyleSheet, View, Text } from 'react-native';
import { MeetingStackParamList } from '../../types';
import { appTheme } from '../theme';
import { useOrientation } from '../hooks/useOrientation';
import { ThermalInfo } from './ThermalInfo';
import { checkLowPowerMode } from './PowerMode';

type LobbyViewComponentType = NativeStackScreenProps<
MeetingStackParamList,
Expand All @@ -28,9 +29,27 @@ export const LobbyViewComponent = ({
const { t } = useI18n();
const orientation = useOrientation();

const [lowPowerMode, setLowPowerMode] = useState(false);

useEffect(() => {
const checkPowerMode = async () => {
const isLowPower = await checkLowPowerMode();
setLowPowerMode(isLowPower);
};

checkPowerMode();

const interval = setInterval(checkLowPowerMode, 5000);

return () => clearInterval(interval);
}, []);

const JoinCallButtonComponent = useCallback(() => {
return (
<>
<Text style={{ color: lowPowerMode ? 'red' : 'green' }}>

Check warning on line 50 in sample-apps/react-native/dogfood/src/components/LobbyViewComponent.tsx

View workflow job for this annotation

GitHub Actions / Code Lint, Unit Test and dogfood versioning

Inline style: { color: "lowPowerMode ? 'red' : 'green'" }
Low Power Mode: {lowPowerMode ? 'Enabled' : 'Disabled'}
</Text>
<ThermalInfo />
<JoinCallButton onPressHandler={onJoinCallHandler} />
{route.name === 'MeetingScreen' ? (
Expand Down Expand Up @@ -58,7 +77,7 @@ export const LobbyViewComponent = ({
)}
</>
);
}, [onJoinCallHandler, callId, navigation, route.name, t]);
}, [onJoinCallHandler, callId, navigation, route.name, t, lowPowerMode]);

return (
<View style={styles.container}>
Expand Down
7 changes: 7 additions & 0 deletions sample-apps/react-native/dogfood/src/components/PowerMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NativeModules } from 'react-native';

const { PowerModeModule } = NativeModules;

export const checkLowPowerMode = (): Promise<boolean> => {
return PowerModeModule.isLowPowerModeEnabled();
};
42 changes: 28 additions & 14 deletions sample-apps/react-native/dogfood/src/components/ThermalInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, Platform, NativeModules } from 'react-native';
import {
View,
Text,
StyleSheet,
Platform,
NativeModules,
NativeEventEmitter,
} from 'react-native';
import { useTheme } from '@stream-io/video-react-native-sdk';

const { ThermalModule } = NativeModules;

const thermalEventEmitter = new NativeEventEmitter(ThermalModule);

export const ThermalInfo = () => {
const [thermalStatus, setThermalStatus] = useState<string>('Unknown');
const { theme } = useTheme();

useEffect(() => {
const checkThermalStatus = async () => {
if (Platform.OS === 'android') {
try {
const status = await ThermalModule.getCurrentThermalStatus();
setThermalStatus(status);
} catch (error) {
console.error('Failed to get thermal status:', error);
if (Platform.OS === 'android') {
// Start listening to thermal status updates
ThermalModule.startThermalStatusUpdates()
.then((initialStatus: string) => setThermalStatus(initialStatus))
.catch((error: any) => {
console.error('Failed to start thermal status updates:', error);
setThermalStatus('Error');
}
}
};
});

checkThermalStatus();
const interval = setInterval(checkThermalStatus, 5000);
// Subscribe to thermal status changes
const subscription = thermalEventEmitter.addListener(
ThermalModule.THERMAL_EVENT,
(status: string) => setThermalStatus(status),
);

return () => clearInterval(interval);
// Cleanup
return () => {
subscription.remove();
ThermalModule.stopThermalStatusUpdates();
};
}
}, []);

const getStatusColor = () => {
Expand Down
21 changes: 21 additions & 0 deletions sample-apps/react-native/dogfood/src/components/ThermalState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NativeEventEmitter, NativeModules } from 'react-native';

export type ThermalState =
| 'unknown'
| 'nominal'
| 'fair'
| 'serious'
| 'critical';

const { ThermalStateModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(ThermalStateModule);

export class ThermalStateManager {
static getCurrentState(): Promise<{ state: ThermalState }> {
return Promise.resolve({ state: 'nominal' });
}

static addListener(callback: (response: { state: ThermalState }) => void) {
return eventEmitter.addListener('ThermalStateChange', callback);
}
}
71 changes: 71 additions & 0 deletions sample-apps/react-native/dogfood/src/components/ThermalStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ThermalState, ThermalStateManager } from './ThermalState';

export const ThermalStatus = () => {
const [thermalState, setThermalState] = useState<ThermalState>('unknown');

useEffect(() => {
// Get initial state
ThermalStateManager.getCurrentState().then((response) => {
setThermalState(response.state);
});

// Listen for changes
const subscription = ThermalStateManager.addListener((response) => {
setThermalState(response.state);
});

return () => subscription.remove();
}, []);

const getStatusColor = (state: ThermalState): string => {
switch (state) {
case 'nominal':
return '#4CAF50'; // Green
case 'fair':
return '#FFC107'; // Yellow
case 'serious':
return '#FF9800'; // Orange
case 'critical':
return '#F44336'; // Red
default:
return '#9E9E9E'; // Grey
}
};

return (
<View style={styles.container}>
<View
style={[
styles.indicator,
{ backgroundColor: getStatusColor(thermalState) },
]}
/>
<Text style={styles.text}>
Device Temperature: <Text style={styles.state}>{thermalState}</Text>
</Text>
</View>
);
};

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
},
indicator: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 8,
},
text: {
fontSize: 16,
},
state: {
textTransform: 'capitalize',
fontWeight: 'bold',
},
});

0 comments on commit b6b7e9b

Please sign in to comment.