Loading services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java +121 −67 Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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<NotificationInfo> 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)); } Loading @@ -72,13 +74,13 @@ class DeviceStateNotificationController extends BroadcastReceiver { DeviceStateNotificationController( @NonNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable, @NonNull SparseArray<NotificationInfo> 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( Loading @@ -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; } Loading Loading @@ -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; } Loading @@ -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; } Loading @@ -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); Loading Loading @@ -221,39 +223,89 @@ class DeviceStateNotificationController extends BroadcastReceiver { mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build()); } private SparseArray<NotificationInfo> getNotificationInfos() { Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); return mNotificationInfoProvider.getNotificationInfos(locale); } @VisibleForTesting public static class NotificationInfoProvider { @NonNull private final Context mContext; private final Object mLock = new Object(); @GuardedBy("mLock") @Nullable private SparseArray<NotificationInfo> mCachedNotificationInfos; @GuardedBy("mLock") @Nullable @VisibleForTesting Locale mCachedLocale; NotificationInfoProvider(@NonNull Context context) { mContext = context; } /** * 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<NotificationInfo> getNotificationInfos(Context context) { @NonNull public SparseArray<NotificationInfo> 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<NotificationInfo> loadNotificationInfos() { final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>(); final int[] stateIdentifiers = context.getResources().getIntArray( mContext.getResources().getIntArray( R.array.device_state_notification_state_identifiers); final String[] names = context.getResources().getStringArray(R.array.device_state_notification_names); mContext.getResources().getStringArray(R.array.device_state_notification_names); final String[] activeNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_active_titles); final String[] activeNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_active_contents); final String[] thermalCriticalNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_thermal_titles); final String[] thermalCriticalNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_thermal_contents); final String[] powerSaveModeNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_power_save_titles); final String[] powerSaveModeNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_power_save_contents); if (stateIdentifiers.length != names.length || stateIdentifiers.length != activeNotificationTitles.length || stateIdentifiers.length != activeNotificationContents.length Loading @@ -275,16 +327,18 @@ class DeviceStateNotificationController extends BroadcastReceiver { notificationInfos.put( identifier, new NotificationInfo( names[i], activeNotificationTitles[i], activeNotificationContents[i], names[i], activeNotificationTitles[i], activeNotificationContents[i], thermalCriticalNotificationTitles[i], thermalCriticalNotificationContents[i], powerSaveModeNotificationTitles[i], powerSaveModeNotificationContents[i]) ); } return notificationInfos; } } /** * A helper function to get app name (label) using the app uid. Loading services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java +37 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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}. * <p/> Loading Loading @@ -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(); Loading @@ -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())) Loading @@ -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); } Loading Loading @@ -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); } } Loading
services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java +121 −67 Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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<NotificationInfo> 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)); } Loading @@ -72,13 +74,13 @@ class DeviceStateNotificationController extends BroadcastReceiver { DeviceStateNotificationController( @NonNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable, @NonNull SparseArray<NotificationInfo> 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( Loading @@ -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; } Loading Loading @@ -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; } Loading @@ -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; } Loading @@ -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); Loading Loading @@ -221,39 +223,89 @@ class DeviceStateNotificationController extends BroadcastReceiver { mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build()); } private SparseArray<NotificationInfo> getNotificationInfos() { Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); return mNotificationInfoProvider.getNotificationInfos(locale); } @VisibleForTesting public static class NotificationInfoProvider { @NonNull private final Context mContext; private final Object mLock = new Object(); @GuardedBy("mLock") @Nullable private SparseArray<NotificationInfo> mCachedNotificationInfos; @GuardedBy("mLock") @Nullable @VisibleForTesting Locale mCachedLocale; NotificationInfoProvider(@NonNull Context context) { mContext = context; } /** * 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<NotificationInfo> getNotificationInfos(Context context) { @NonNull public SparseArray<NotificationInfo> 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<NotificationInfo> loadNotificationInfos() { final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>(); final int[] stateIdentifiers = context.getResources().getIntArray( mContext.getResources().getIntArray( R.array.device_state_notification_state_identifiers); final String[] names = context.getResources().getStringArray(R.array.device_state_notification_names); mContext.getResources().getStringArray(R.array.device_state_notification_names); final String[] activeNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_active_titles); final String[] activeNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_active_contents); final String[] thermalCriticalNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_thermal_titles); final String[] thermalCriticalNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_thermal_contents); final String[] powerSaveModeNotificationTitles = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_power_save_titles); final String[] powerSaveModeNotificationContents = context.getResources().getStringArray( mContext.getResources().getStringArray( R.array.device_state_notification_power_save_contents); if (stateIdentifiers.length != names.length || stateIdentifiers.length != activeNotificationTitles.length || stateIdentifiers.length != activeNotificationContents.length Loading @@ -275,16 +327,18 @@ class DeviceStateNotificationController extends BroadcastReceiver { notificationInfos.put( identifier, new NotificationInfo( names[i], activeNotificationTitles[i], activeNotificationContents[i], names[i], activeNotificationTitles[i], activeNotificationContents[i], thermalCriticalNotificationTitles[i], thermalCriticalNotificationContents[i], powerSaveModeNotificationTitles[i], powerSaveModeNotificationContents[i]) ); } return notificationInfos; } } /** * A helper function to get app name (label) using the app uid. Loading
services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java +37 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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}. * <p/> Loading Loading @@ -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(); Loading @@ -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())) Loading @@ -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); } Loading Loading @@ -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); } }