From ca84d3d6c21b632144d53b801c35f1361e336ac1 Mon Sep 17 00:00:00 2001 From: Jiaming Liu Date: Wed, 29 Mar 2023 21:05:24 +0000 Subject: [PATCH] Fix translation for device state notifications Reloads notification strings when locale has changed. Previously, the strings were loaded during boot time and did not update when device locale was changed. Fix: 275423060 Test: atest DeviceStateNotificationControllerTest Change-Id: I0b8bd54a26a9c2e3dd61b991dba14559c01a7aac --- .../DeviceStateNotificationController.java | 184 +++++++++++------- ...DeviceStateNotificationControllerTest.java | 38 +++- 2 files changed, 156 insertions(+), 66 deletions(-) diff --git a/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java index ab261ac240910..f4c84e7fe464c 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java @@ -37,8 +37,11 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import java.util.Locale; + /** * Manages the user-visible device state notifications. */ @@ -56,15 +59,14 @@ class DeviceStateNotificationController extends BroadcastReceiver { private final NotificationManager mNotificationManager; private final PackageManager mPackageManager; - // Stores the notification title and content indexed with the device state identifier. - private final SparseArray mNotificationInfos; - // The callback when a device state is requested to be canceled. private final Runnable mCancelStateRunnable; + private final NotificationInfoProvider mNotificationInfoProvider; + DeviceStateNotificationController(@NonNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable) { - this(context, handler, cancelStateRunnable, getNotificationInfos(context), + this(context, handler, cancelStateRunnable, new NotificationInfoProvider(context), context.getPackageManager(), context.getSystemService(NotificationManager.class)); } @@ -72,13 +74,13 @@ class DeviceStateNotificationController extends BroadcastReceiver { DeviceStateNotificationController( @NonNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable, - @NonNull SparseArray notificationInfos, + @NonNull NotificationInfoProvider notificationInfoProvider, @NonNull PackageManager packageManager, @NonNull NotificationManager notificationManager) { mContext = context; mHandler = handler; mCancelStateRunnable = cancelStateRunnable; - mNotificationInfos = notificationInfos; + mNotificationInfoProvider = notificationInfoProvider; mPackageManager = packageManager; mNotificationManager = notificationManager; mContext.registerReceiver( @@ -97,7 +99,7 @@ class DeviceStateNotificationController extends BroadcastReceiver { * @param requestingAppUid the uid of the requesting app used to retrieve the app name. */ void showStateActiveNotificationIfNeeded(int state, int requestingAppUid) { - NotificationInfo info = mNotificationInfos.get(state); + NotificationInfo info = getNotificationInfos().get(state); if (info == null || !info.hasActiveNotification()) { return; } @@ -127,7 +129,7 @@ class DeviceStateNotificationController extends BroadcastReceiver { * @param state the identifier of the device state being canceled. */ void showThermalCriticalNotificationIfNeeded(int state) { - NotificationInfo info = mNotificationInfos.get(state); + NotificationInfo info = getNotificationInfos().get(state); if (info == null || !info.hasThermalCriticalNotification()) { return; } @@ -148,7 +150,7 @@ class DeviceStateNotificationController extends BroadcastReceiver { * @param state the identifier of the device state being canceled. */ void showPowerSaveNotificationIfNeeded(int state) { - NotificationInfo info = mNotificationInfos.get(state); + NotificationInfo info = getNotificationInfos().get(state); if (info == null || !info.hasPowerSaveModeNotification()) { return; } @@ -170,7 +172,7 @@ class DeviceStateNotificationController extends BroadcastReceiver { * @param state the device state identifier. */ void cancelNotification(int state) { - if (!mNotificationInfos.contains(state)) { + if (getNotificationInfos().get(state) == null) { return; } mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID); @@ -221,69 +223,121 @@ class DeviceStateNotificationController extends BroadcastReceiver { mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build()); } - /** - * Loads the resources for the notifications. The device state identifiers and strings are - * stored in arrays. All the string arrays must have the same length and same order as the - * identifier array. - */ - private static SparseArray getNotificationInfos(Context context) { - final SparseArray notificationInfos = new SparseArray<>(); + private SparseArray getNotificationInfos() { + Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); + return mNotificationInfoProvider.getNotificationInfos(locale); + } - final int[] stateIdentifiers = - context.getResources().getIntArray( - R.array.device_state_notification_state_identifiers); - final String[] names = - context.getResources().getStringArray(R.array.device_state_notification_names); - final String[] activeNotificationTitles = - context.getResources().getStringArray( - R.array.device_state_notification_active_titles); - final String[] activeNotificationContents = - context.getResources().getStringArray( - R.array.device_state_notification_active_contents); - final String[] thermalCriticalNotificationTitles = - context.getResources().getStringArray( - R.array.device_state_notification_thermal_titles); - final String[] thermalCriticalNotificationContents = - context.getResources().getStringArray( - R.array.device_state_notification_thermal_contents); - final String[] powerSaveModeNotificationTitles = - context.getResources().getStringArray( - R.array.device_state_notification_power_save_titles); - final String[] powerSaveModeNotificationContents = - context.getResources().getStringArray( - R.array.device_state_notification_power_save_contents); + @VisibleForTesting + public static class NotificationInfoProvider { + @NonNull + private final Context mContext; + private final Object mLock = new Object(); + @GuardedBy("mLock") + @Nullable + private SparseArray mCachedNotificationInfos; - if (stateIdentifiers.length != names.length - || stateIdentifiers.length != activeNotificationTitles.length - || stateIdentifiers.length != activeNotificationContents.length - || stateIdentifiers.length != thermalCriticalNotificationTitles.length - || stateIdentifiers.length != thermalCriticalNotificationContents.length - || stateIdentifiers.length != powerSaveModeNotificationTitles.length - || stateIdentifiers.length != powerSaveModeNotificationContents.length - ) { - throw new IllegalStateException( - "The length of state identifiers and notification texts must match!"); + @GuardedBy("mLock") + @Nullable + @VisibleForTesting + Locale mCachedLocale; + + NotificationInfoProvider(@NonNull Context context) { + mContext = context; } - for (int i = 0; i < stateIdentifiers.length; i++) { - int identifier = stateIdentifiers[i]; - if (identifier == DeviceStateManager.INVALID_DEVICE_STATE) { - continue; + /** + * Loads the resources for the notifications. The device state identifiers and strings are + * stored in arrays. All the string arrays must have the same length and same order as the + * identifier array. + */ + @NonNull + public SparseArray getNotificationInfos(@NonNull Locale locale) { + synchronized (mLock) { + if (!locale.equals(mCachedLocale)) { + refreshNotificationInfos(locale); + } + return mCachedNotificationInfos; + } + } + + + @VisibleForTesting + Locale getCachedLocale() { + synchronized (mLock) { + return mCachedLocale; + } + } + + @VisibleForTesting + public void refreshNotificationInfos(Locale locale) { + synchronized (mLock) { + mCachedLocale = locale; + mCachedNotificationInfos = loadNotificationInfos(); + } + } + + @VisibleForTesting + public SparseArray loadNotificationInfos() { + final SparseArray notificationInfos = new SparseArray<>(); + + final int[] stateIdentifiers = + mContext.getResources().getIntArray( + R.array.device_state_notification_state_identifiers); + final String[] names = + mContext.getResources().getStringArray(R.array.device_state_notification_names); + final String[] activeNotificationTitles = + mContext.getResources().getStringArray( + R.array.device_state_notification_active_titles); + final String[] activeNotificationContents = + mContext.getResources().getStringArray( + R.array.device_state_notification_active_contents); + final String[] thermalCriticalNotificationTitles = + mContext.getResources().getStringArray( + R.array.device_state_notification_thermal_titles); + final String[] thermalCriticalNotificationContents = + mContext.getResources().getStringArray( + R.array.device_state_notification_thermal_contents); + final String[] powerSaveModeNotificationTitles = + mContext.getResources().getStringArray( + R.array.device_state_notification_power_save_titles); + final String[] powerSaveModeNotificationContents = + mContext.getResources().getStringArray( + R.array.device_state_notification_power_save_contents); + + if (stateIdentifiers.length != names.length + || stateIdentifiers.length != activeNotificationTitles.length + || stateIdentifiers.length != activeNotificationContents.length + || stateIdentifiers.length != thermalCriticalNotificationTitles.length + || stateIdentifiers.length != thermalCriticalNotificationContents.length + || stateIdentifiers.length != powerSaveModeNotificationTitles.length + || stateIdentifiers.length != powerSaveModeNotificationContents.length + ) { + throw new IllegalStateException( + "The length of state identifiers and notification texts must match!"); } - notificationInfos.put( - identifier, - new NotificationInfo( - names[i], activeNotificationTitles[i], activeNotificationContents[i], - thermalCriticalNotificationTitles[i], - thermalCriticalNotificationContents[i], - powerSaveModeNotificationTitles[i], - powerSaveModeNotificationContents[i]) - ); - } + for (int i = 0; i < stateIdentifiers.length; i++) { + int identifier = stateIdentifiers[i]; + if (identifier == DeviceStateManager.INVALID_DEVICE_STATE) { + continue; + } - return notificationInfos; + notificationInfos.put( + identifier, + new NotificationInfo( + names[i], + activeNotificationTitles[i], + activeNotificationContents[i], + thermalCriticalNotificationTitles[i], + thermalCriticalNotificationContents[i], + powerSaveModeNotificationTitles[i], + powerSaveModeNotificationContents[i]) + ); + } + return notificationInfos; + } } /** diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java index e396263b1679d..728606f9f6285 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java @@ -17,8 +17,13 @@ package com.android.server.devicestate; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,6 +46,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import java.util.Locale; + /** * Unit tests for {@link DeviceStateNotificationController}. *

@@ -77,6 +84,8 @@ public class DeviceStateNotificationControllerTest { Notification.class); private final NotificationManager mNotificationManager = mock(NotificationManager.class); + private DeviceStateNotificationController.NotificationInfoProvider mNotificationInfoProvider; + @Before public void setup() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getContext(); @@ -97,6 +106,11 @@ public class DeviceStateNotificationControllerTest { THERMAL_TITLE_2, THERMAL_CONTENT_2, POWER_SAVE_TITLE_2, POWER_SAVE_CONTENT_2)); + mNotificationInfoProvider = + new DeviceStateNotificationController.NotificationInfoProvider(context); + mNotificationInfoProvider = spy(mNotificationInfoProvider); + doReturn(notificationInfos).when(mNotificationInfoProvider).loadNotificationInfos(); + when(packageManager.getNameForUid(VALID_APP_UID)).thenReturn(VALID_APP_NAME); when(packageManager.getNameForUid(INVALID_APP_UID)).thenReturn(INVALID_APP_NAME); when(packageManager.getApplicationInfo(eq(VALID_APP_NAME), ArgumentMatchers.any())) @@ -106,7 +120,7 @@ public class DeviceStateNotificationControllerTest { when(applicationInfo.loadLabel(eq(packageManager))).thenReturn(VALID_APP_LABEL); mController = new DeviceStateNotificationController( - context, handler, cancelStateRunnable, notificationInfos, + context, handler, cancelStateRunnable, mNotificationInfoProvider, packageManager, mNotificationManager); } @@ -223,4 +237,26 @@ public class DeviceStateNotificationControllerTest { eq(DeviceStateNotificationController.NOTIFICATION_ID), mNotificationCaptor.capture()); } + + @Test + public void test_notificationInfoProvider() { + assertNull(mNotificationInfoProvider.getCachedLocale()); + + mNotificationInfoProvider.getNotificationInfos(Locale.ENGLISH); + verify(mNotificationInfoProvider).refreshNotificationInfos(eq(Locale.ENGLISH)); + assertEquals(Locale.ENGLISH, mNotificationInfoProvider.getCachedLocale()); + clearInvocations(mNotificationInfoProvider); + + // If the same locale is used again, the provider uses the cached value, so it won't refresh + mNotificationInfoProvider.getNotificationInfos(Locale.ENGLISH); + verify(mNotificationInfoProvider, never()).refreshNotificationInfos(eq(Locale.ENGLISH)); + assertEquals(Locale.ENGLISH, mNotificationInfoProvider.getCachedLocale()); + clearInvocations(mNotificationInfoProvider); + + // If a different locale is used, the provider refreshes. + mNotificationInfoProvider.getNotificationInfos(Locale.ITALY); + verify(mNotificationInfoProvider).refreshNotificationInfos(eq(Locale.ITALY)); + assertEquals(Locale.ITALY, mNotificationInfoProvider.getCachedLocale()); + clearInvocations(mNotificationInfoProvider); + } }