From e75a66e87a4c4cf59f0ea6f4bd509fd707c2383f Mon Sep 17 00:00:00 2001 From: Geoffrey Pitsch Date: Tue, 22 Nov 2016 11:12:11 -0500 Subject: [PATCH] Listener in createNotificationChannel Necessary for when this will eventually trigger an Activity. New unit test file for NotificationServiceManager. Test: runtest systemui-notification (cts tests in separate CL) Change-Id: I8f3e8e34ddcebb1acb9ddd84bffc68affb4b6e89 --- Android.mk | 1 + api/current.txt | 6 +- api/system-current.txt | 6 +- api/test-current.txt | 6 +- .../android/app/INotificationManager.aidl | 20 ++-- ...IOnNotificationChannelCreatedListener.aidl | 25 +++++ .../java/android/app/NotificationManager.java | 40 +++++++- .../NotificationManagerService.java | 46 ++++++--- .../NotificationManagerServiceTest.java | 96 +++++++++++++++++++ 9 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 core/java/android/app/IOnNotificationChannelCreatedListener.aidl create mode 100644 services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java diff --git a/Android.mk b/Android.mk index 7103f67862daa..5ea666c312a7e 100644 --- a/Android.mk +++ b/Android.mk @@ -79,6 +79,7 @@ LOCAL_SRC_FILES += \ core/java/android/app/IBackupAgent.aidl \ core/java/android/app/IEphemeralResolver.aidl \ core/java/android/app/IInstrumentationWatcher.aidl \ + core/java/android/app/IOnNotificationChannelCreatedListener.aidl \ core/java/android/app/INotificationManager.aidl \ core/java/android/app/IProcessObserver.aidl \ core/java/android/app/ISearchManager.aidl \ diff --git a/api/current.txt b/api/current.txt index 2e543e4634eae..dfb7c8ad03a23 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5387,7 +5387,7 @@ package android.app { method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); - method public void createNotificationChannel(android.app.NotificationChannel); + method public void createNotificationChannel(android.app.NotificationChannel, android.app.NotificationManager.OnNotificationChannelCreatedListener, android.os.Handler); method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); @@ -5421,6 +5421,10 @@ package android.app { field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0 } + public static abstract interface NotificationManager.OnNotificationChannelCreatedListener { + method public abstract void onNotificationChannelCreated(android.app.NotificationChannel); + } + public static class NotificationManager.Policy implements android.os.Parcelable { ctor public NotificationManager.Policy(int, int, int); ctor public NotificationManager.Policy(int, int, int, int); diff --git a/api/system-current.txt b/api/system-current.txt index 613f387f57edb..bdcbdffd588c8 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5554,7 +5554,7 @@ package android.app { method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); - method public void createNotificationChannel(android.app.NotificationChannel); + method public void createNotificationChannel(android.app.NotificationChannel, android.app.NotificationManager.OnNotificationChannelCreatedListener, android.os.Handler); method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); @@ -5588,6 +5588,10 @@ package android.app { field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0 } + public static abstract interface NotificationManager.OnNotificationChannelCreatedListener { + method public abstract void onNotificationChannelCreated(android.app.NotificationChannel); + } + public static class NotificationManager.Policy implements android.os.Parcelable { ctor public NotificationManager.Policy(int, int, int); ctor public NotificationManager.Policy(int, int, int, int); diff --git a/api/test-current.txt b/api/test-current.txt index b321ae6fa5800..49f70dea4f75a 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5397,7 +5397,7 @@ package android.app { method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); - method public void createNotificationChannel(android.app.NotificationChannel); + method public void createNotificationChannel(android.app.NotificationChannel, android.app.NotificationManager.OnNotificationChannelCreatedListener, android.os.Handler); method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); @@ -5432,6 +5432,10 @@ package android.app { field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0 } + public static abstract interface NotificationManager.OnNotificationChannelCreatedListener { + method public abstract void onNotificationChannelCreated(android.app.NotificationChannel); + } + public static class NotificationManager.Policy implements android.os.Parcelable { ctor public NotificationManager.Policy(int, int, int); ctor public NotificationManager.Policy(int, int, int, int); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 2a7341ae6911c..927ef6c35b7ac 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -2,21 +2,22 @@ ** ** Copyright 2007, The Android Open Source Project ** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at ** -** http://www.apache.org/licenses/LICENSE-2.0 +** http://www.apache.org/licenses/LICENSE-2.0 ** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and ** limitations under the License. */ package android.app; +import android.app.IOnNotificationChannelCreatedListener; import android.app.ITransientNotification; import android.app.Notification; import android.app.NotificationChannel; @@ -58,7 +59,8 @@ interface INotificationManager int getImportance(String pkg, int uid); int getPackageImportance(String pkg); - void createNotificationChannel(String pkg, in NotificationChannel channel); + void createNotificationChannel(String pkg, in NotificationChannel channel, + in IOnNotificationChannelCreatedListener listener); void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel); NotificationChannel getNotificationChannel(String pkg, String channelId); NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId); diff --git a/core/java/android/app/IOnNotificationChannelCreatedListener.aidl b/core/java/android/app/IOnNotificationChannelCreatedListener.aidl new file mode 100644 index 0000000000000..8e6654200017a --- /dev/null +++ b/core/java/android/app/IOnNotificationChannelCreatedListener.aidl @@ -0,0 +1,25 @@ +/* +** +** Copyright 2016, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.app; + +import android.app.NotificationChannel; + +/** {@hide} */ +oneway interface IOnNotificationChannelCreatedListener { + void onNotificationChannelCreated(in NotificationChannel channel); +} diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 047f349b1e540..7693d7666e19e 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -18,6 +18,7 @@ package android.app; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.TestApi; import android.app.Notification.Builder; @@ -30,6 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -379,12 +381,42 @@ public class NotificationManager } /** - * Creates a notification channel that notifications can be posted to. + * Listener passed to {@link NotificationManager#createNotificationChannel} to notify + * caller of result. */ - public void createNotificationChannel(NotificationChannel channel) { + public interface OnNotificationChannelCreatedListener { + /** + * @param createdChannel NotificationChannel created by the system. Value is null iff an + * exception was thrown during channel creation. + */ + public void onNotificationChannelCreated(NotificationChannel createdChannel); + } + + /** + * Creates a notification channel that notifications can be posted to. + * + * @param channel the channel to attempt to create. Note that the created channel may differ + * from this value. + * @param listener Called when operation is finished. + * @param handler The handler to invoke the listener on, or {@code null} to use the main + * handler. + */ + public void createNotificationChannel( + @NonNull NotificationChannel channel, + @NonNull OnNotificationChannelCreatedListener listener, + @Nullable Handler handler) { INotificationManager service = getService(); try { - service.createNotificationChannel(mContext.getPackageName(), channel); + final Handler actualHandler = + handler != null ? handler : new Handler(Looper.getMainLooper()); + service.createNotificationChannel(mContext.getPackageName(), channel, + new IOnNotificationChannelCreatedListener.Stub() { + @Override public void onNotificationChannelCreated( + NotificationChannel channel) { + actualHandler.post( + () -> { listener.onNotificationChannelCreated(channel); }); + } + }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -583,7 +615,7 @@ public class NotificationManager *

* Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}. * @param id The id of the rule to update - * @param automaticZenRule the rule to update. + * @param automaticZenRule the rule to update. * @return Whether the rule was successfully updated. */ public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 84c298b06284d..c78a0f57b0ca1 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -53,16 +53,17 @@ import android.app.ActivityManagerInternal; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AutomaticZenRule; +import android.app.backup.BackupManager; import android.app.IActivityManager; +import android.app.IOnNotificationChannelCreatedListener; import android.app.INotificationManager; import android.app.ITransientNotification; import android.app.Notification; import android.app.NotificationChannel; -import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.StatusBarManager; -import android.app.backup.BackupManager; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.content.BroadcastReceiver; @@ -224,6 +225,7 @@ public class NotificationManagerService extends SystemService { private static final long MIN_PACKAGE_OVERRATE_LOG_INTERVAL = 5000; // milliseconds private IActivityManager mAm; + private IPackageManager mPackageManager; AudioManager mAudioManager; AudioManagerInternal mAudioManagerInternal; @Nullable StatusBarManagerInternal mStatusBar; @@ -718,10 +720,10 @@ public class NotificationManagerService extends SystemService { if (packageChanged) { // We cancel notifications for packages which have just been disabled try { - final IPackageManager pm = AppGlobals.getPackageManager(); - final int enabled = pm.getApplicationEnabledSetting(pkgName, + final int enabled = mPackageManager.getApplicationEnabledSetting( + pkgName, changeUserId != UserHandle.USER_ALL ? changeUserId : - UserHandle.USER_SYSTEM); + UserHandle.USER_SYSTEM); if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { cancelNotifications = false; @@ -884,6 +886,7 @@ public class NotificationManagerService extends SystemService { super(context); } + // TODO - replace these methods with a single VisibleForTesting constructor @VisibleForTesting void setAudioManager(AudioManager audioMananger) { mAudioManager = audioMananger; @@ -931,6 +934,17 @@ public class NotificationManagerService extends SystemService { mFallbackVibrationPattern = vibrationPattern; } + @VisibleForTesting + void setPackageManager(IPackageManager packageManager) { + mPackageManager = packageManager; + } + + // TODO: This probably should not be mocked, it's an implementation detail. + @VisibleForTesting + void setRankingHelper(RankingHelper rankingHelper) { + mRankingHelper = rankingHelper; + } + @Override public void onStart() { Resources resources = getContext().getResources(); @@ -940,6 +954,7 @@ public class NotificationManagerService extends SystemService { DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE); mAm = ActivityManager.getService(); + mPackageManager = AppGlobals.getPackageManager(); mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); @@ -1312,6 +1327,11 @@ public class NotificationManagerService extends SystemService { scheduleInterruptionFilterChanged(interruptionFilter); } + @VisibleForTesting + INotificationManager getBinderService() { + return INotificationManager.Stub.asInterface(mService); + } + private final IBinder mService = new INotificationManager.Stub() { // Toasts // ============================================================================ @@ -1529,13 +1549,15 @@ public class NotificationManagerService extends SystemService { } @Override - public void createNotificationChannel(String pkg, NotificationChannel channel) { + public void createNotificationChannel(String pkg, NotificationChannel channel, + IOnNotificationChannelCreatedListener listener) throws RemoteException { Preconditions.checkNotNull(channel); Preconditions.checkNotNull(channel.getId()); Preconditions.checkNotNull(channel.getName()); checkCallerIsSystemOrSameApp(pkg); mRankingHelper.createNotificationChannel(pkg, Binder.getCallingUid(), channel); savePolicyFile(); + listener.onNotificationChannelCreated(channel); } @Override @@ -3906,23 +3928,23 @@ public class NotificationManagerService extends SystemService { throw new SecurityException("Disallowed call for uid " + Binder.getCallingUid()); } - private static void checkCallerIsSystemOrSameApp(String pkg) { + private void checkCallerIsSystemOrSameApp(String pkg) { if (isCallerSystem()) { return; } checkCallerIsSameApp(pkg); } - private static void checkCallerIsSameApp(String pkg) { + private void checkCallerIsSameApp(String pkg) { final int uid = Binder.getCallingUid(); try { - ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo( + ApplicationInfo ai = mPackageManager.getApplicationInfo( pkg, 0, UserHandle.getCallingUserId()); if (ai == null) { throw new SecurityException("Unknown package " + pkg); } if (!UserHandle.isSameApp(ai.uid, uid)) { - throw new SecurityException("Calling uid " + uid + " gave package" + throw new SecurityException("Calling uid " + uid + " gave package " + pkg + " which is owned by uid " + ai.uid); } } catch (RemoteException re) { @@ -4009,7 +4031,7 @@ public class NotificationManagerService extends SystemService { private boolean isPackageSuspendedForUser(String pkg, int uid) { int userId = UserHandle.getUserId(uid); try { - return AppGlobals.getPackageManager().isPackageSuspendedForUser(pkg, userId); + return mPackageManager.isPackageSuspendedForUser(pkg, userId); } catch (RemoteException re) { throw new SecurityException("Could not talk to package manager service"); } catch (IllegalArgumentException ex) { @@ -4510,7 +4532,7 @@ public class NotificationManagerService extends SystemService { } public String[] getRequestingPackages() throws RemoteException { - final ParceledListSlice list = AppGlobals.getPackageManager() + final ParceledListSlice list = mPackageManager .getPackagesHoldingPermissions(PERM, 0 /*flags*/, ActivityManager.getCurrentUser()); final List pkgs = list.getList(); diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java new file mode 100644 index 0000000000000..e4a355f632f48 --- /dev/null +++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static junit.framework.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.INotificationManager; +import android.app.IOnNotificationChannelCreatedListener; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.os.Binder; +import android.os.Handler; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; +import java.util.concurrent.CountDownLatch; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NotificationManagerServiceTest { + private NotificationManagerService mNotificationManagerService; + private INotificationManager mBinderService; + + @Before + public void setUp() throws Exception { + final Context context = InstrumentationRegistry.getTargetContext(); + mNotificationManagerService = new NotificationManagerService(context); + + // MockPackageManager - default returns ApplicationInfo with matching calling UID + final IPackageManager mockPackageManager = mock(IPackageManager.class); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = Binder.getCallingUid(); + when(mockPackageManager.getApplicationInfo(any(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + mNotificationManagerService.setPackageManager(mockPackageManager); + + mNotificationManagerService.setRankingHelper(mock(RankingHelper.class)); + mNotificationManagerService.setHandler(new Handler(context.getMainLooper())); + + // Tests call directly into the Binder. + mBinderService = mNotificationManagerService.getBinderService(); + } + + @Test + public void testCreateNotificationChannel_SuccessCallsListener() throws Exception { + final NotificationChannel channel = + new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT); + final CountDownLatch latch = new CountDownLatch(1); + mBinderService.createNotificationChannel("test_pkg", channel, + new IOnNotificationChannelCreatedListener.Stub() { + @Override public void onNotificationChannelCreated( + NotificationChannel channel) { + latch.countDown(); + }}); + latch.await(); + } + + @Test + public void testCreateNotificationChannel_FailureDoesNotCallListener() throws Exception { + try { + mBinderService.createNotificationChannel("test_pkg", null, + new IOnNotificationChannelCreatedListener.Stub() { + @Override public void onNotificationChannelCreated( + NotificationChannel channel) { + fail("Listener was triggered from failure."); + }}); + fail("Exception should be thrown immediately."); + } catch (NullPointerException e) { + // pass + } + } +}