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); + } }