Merge "Track and destroy inline URI grants separately from Notification URIs."
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 499a4d2..d703b86 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -21,6 +21,7 @@
 import android.content.ComponentName;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.hardware.biometrics.IBiometricServiceReceiverInternal;
 
@@ -78,7 +79,8 @@
     void onNotificationSettingsViewed(String key);
     void setSystemUiVisibility(int displayId, int vis, int mask, String cause);
     void onNotificationBubbleChanged(String key, boolean isBubble);
-    void grantInlineReplyUriPermission(String key, in Uri uri);
+    void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName);
+    void clearInlineReplyUriPermissions(String key);
 
     void onGlobalActionsShown();
     void onGlobalActionsHidden();
diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
index 3acbc3a..706727b 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
@@ -89,6 +89,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.tv.TvStatusBar;
@@ -290,7 +291,8 @@
             DozeScrimController dozeScrimController,
             CommandQueue commandQueue,
             PluginManager pluginManager,
-            CarNavigationBarController carNavigationBarController) {
+            CarNavigationBarController carNavigationBarController,
+            RemoteInputUriController remoteInputUriController) {
         return new CarStatusBar(
                 context,
                 featureFlags,
@@ -357,6 +359,7 @@
                 dozeScrimController,
                 commandQueue,
                 pluginManager,
+                remoteInputUriController,
                 carNavigationBarController);
     }
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index 7ab2036..110b32b 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -128,6 +128,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.util.InjectionInflationController;
@@ -301,7 +302,7 @@
             DozeScrimController dozeScrimController,
             CommandQueue commandQueue,
             PluginManager pluginManager,
-
+            RemoteInputUriController remoteInputUriController,
             /* Car Settings injected components. */
             CarNavigationBarController carNavigationBarController) {
         super(
@@ -370,7 +371,8 @@
                 powerManager,
                 dozeScrimController,
                 commandQueue,
-                pluginManager);
+                pluginManager,
+                remoteInputUriController);
         mScrimController = scrimController;
         mCarNavigationBarController = carNavigationBarController;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
index 4f429d3..3b0c9ae 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
@@ -96,6 +96,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.tv.TvStatusBar;
@@ -287,7 +288,8 @@
             PowerManager powerManager,
             DozeScrimController dozeScrimController,
             CommandQueue commandQueue,
-            PluginManager pluginManager) {
+            PluginManager pluginManager,
+            RemoteInputUriController remoteInputUriController) {
         return new StatusBar(
                 context,
                 featureFlags,
@@ -354,7 +356,8 @@
                 powerManager,
                 dozeScrimController,
                 commandQueue,
-                pluginManager);
+                pluginManager,
+                remoteInputUriController);
     }
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index c838ac5..35f06f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -59,6 +59,7 @@
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.phone.ShadeController;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.RemoteInputView;
 
 import java.io.FileDescriptor;
@@ -121,6 +122,7 @@
     private final UserManager mUserManager;
     private final KeyguardManager mKeyguardManager;
     private final StatusBarStateController mStatusBarStateController;
+    private final RemoteInputUriController mRemoteInputUriController;
 
     protected RemoteInputController mRemoteInputController;
     protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
@@ -260,7 +262,8 @@
             NotificationEntryManager notificationEntryManager,
             Lazy<ShadeController> shadeController,
             StatusBarStateController statusBarStateController,
-            @MainHandler Handler mainHandler) {
+            @MainHandler Handler mainHandler,
+            RemoteInputUriController remoteInputUriController) {
         mContext = context;
         mLockscreenUserManager = lockscreenUserManager;
         mSmartReplyController = smartReplyController;
@@ -273,6 +276,7 @@
         addLifetimeExtenders();
         mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mStatusBarStateController = statusBarStateController;
+        mRemoteInputUriController = remoteInputUriController;
 
         notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
             @Override
@@ -300,7 +304,7 @@
     /** Initializes this component with the provided dependencies. */
     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
         mCallback = callback;
-        mRemoteInputController = new RemoteInputController(delegate);
+        mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
             @Override
             public void onRemoteInputSent(NotificationEntry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
index 998cf52..778443c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
@@ -19,12 +19,15 @@
 import android.app.Notification;
 import android.app.RemoteInput;
 import android.content.Context;
+import android.net.Uri;
 import android.os.SystemProperties;
+import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 import android.util.Pair;
 
 import com.android.internal.util.Preconditions;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.RemoteInputView;
 
 import java.lang.ref.WeakReference;
@@ -43,9 +46,12 @@
     private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
     private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
     private final Delegate mDelegate;
+    private final RemoteInputUriController mRemoteInputUriController;
 
-    public RemoteInputController(Delegate delegate) {
+    public RemoteInputController(Delegate delegate,
+            RemoteInputUriController remoteInputUriController) {
         mDelegate = delegate;
+        mRemoteInputUriController = remoteInputUriController;
     }
 
     /**
@@ -272,6 +278,14 @@
         mDelegate.lockScrollTo(entry);
     }
 
+    /**
+     * Create a temporary grant which allows the app that submitted the notification access to the
+     * specified URI.
+     */
+    public void grantInlineReplyUriPermission(StatusBarNotification sbn, Uri data) {
+        mRemoteInputUriController.grantInlineReplyUriPermission(sbn, data);
+    }
+
     public interface Callback {
         default void onRemoteInputActive(boolean active) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 8f13741..4148a73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -238,6 +238,7 @@
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -401,6 +402,7 @@
     private final DozeParameters mDozeParameters;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     private final PluginManager mPluginManager;
+    private final RemoteInputUriController mRemoteInputUriController;
 
     // expanded notifications
     protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
@@ -703,7 +705,8 @@
             PowerManager powerManager,
             DozeScrimController dozeScrimController,
             CommandQueue commandQueue,
-            PluginManager pluginManager) {
+            PluginManager pluginManager,
+            RemoteInputUriController remoteInputUriController) {
         super(context);
         mFeatureFlags = featureFlags;
         mLightBarController = lightBarController;
@@ -770,7 +773,7 @@
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
         mCommandQueue = commandQueue;
         mPluginManager = pluginManager;
-
+        mRemoteInputUriController = remoteInputUriController;
         mBubbleExpandListener =
                 (isExpanding, key) -> {
                     mEntryManager.updateNotifications("onBubbleExpandChanged");
@@ -1296,6 +1299,8 @@
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
 
         mEntryManager.setRowBinder(rowBinder);
+        mRemoteInputUriController.attach(mEntryManager);
+
         rowBinder.setNotificationClicker(new NotificationClicker(
                 this, mBubbleController, mNotificationActivityStarter));
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputUriController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputUriController.java
new file mode 100644
index 0000000..4d912de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputUriController.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 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.systemui.statusbar.policy;
+
+import android.net.Uri;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.statusbar.notification.NotificationEntryListener;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Handles granting and revoking inline URI grants associated with RemoteInputs.
+ */
+@Singleton
+public class RemoteInputUriController {
+
+    private final IStatusBarService mStatusBarManagerService;
+    private static final String TAG = "RemoteInputUriController";
+
+    @Inject
+    public RemoteInputUriController(IStatusBarService statusBarService) {
+        mStatusBarManagerService = statusBarService;
+    }
+
+    /**
+     * Attach this controller as a listener to the provided NotificationEntryManager to ensure
+     * that RemoteInput URI grants are cleaned up when the notification entry is removed from
+     * the shade.
+     */
+    public void attach(NotificationEntryManager manager) {
+        manager.addNotificationEntryListener(mInlineUriListener);
+    }
+
+    /**
+     * Create a temporary grant which allows the app that submitted the notification access to the
+     * specified URI.
+     */
+    public void grantInlineReplyUriPermission(StatusBarNotification sbn, Uri data) {
+        try {
+            mStatusBarManagerService.grantInlineReplyUriPermission(
+                    sbn.getKey(), data, sbn.getUser(), sbn.getPackageName());
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to grant URI permissions:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Ensures that inline URI permissions are cleared when notification entries are removed from
+     * the shade.
+     */
+    private final NotificationEntryListener mInlineUriListener = new NotificationEntryListener() {
+        @Override
+        public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility,
+                boolean removedByUser) {
+            try {
+                mStatusBarManagerService.clearInlineReplyUriPermissions(entry.getKey());
+            } catch (RemoteException ex) {
+                ex.rethrowFromSystemServer();
+            }
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 502a9bd..307e3bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -173,12 +173,7 @@
     protected Intent prepareRemoteInputFromData(String contentType, Uri data) {
         HashMap<String, Uri> results = new HashMap<>();
         results.put(contentType, data);
-        try {
-            mStatusBarManagerService.grantInlineReplyUriPermission(
-                    mEntry.getSbn().getKey(), data);
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to grant URI permissions:" + e.getMessage(), e);
-        }
+        mController.grantInlineReplyUriPermission(mEntry.getSbn(), data);
         Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         RemoteInput.addDataResultToIntent(mRemoteInput, fillInIntent, results);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 2514c93..a754a00d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.phone.ShadeController;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 
 import com.google.android.collect.Sets;
 
@@ -57,6 +58,7 @@
     @Mock private NotificationListenerService.RankingMap mRanking;
     @Mock private ExpandableNotificationRow mRow;
     @Mock private StatusBarStateController mStateController;
+    @Mock private RemoteInputUriController mRemoteInputUriController;
 
     // Dependency mocks:
     @Mock private NotificationEntryManager mEntryManager;
@@ -76,7 +78,8 @@
                 mLockscreenUserManager, mSmartReplyController, mEntryManager,
                 () -> mock(ShadeController.class),
                 mStateController,
-                Handler.createAsync(Looper.myLooper()));
+                Handler.createAsync(Looper.myLooper()),
+                mRemoteInputUriController);
         mEntry = new NotificationEntryBuilder()
                 .setPkg(TEST_PACKAGE_NAME)
                 .setOpPkg(TEST_PACKAGE_NAME)
@@ -211,9 +214,11 @@
                 NotificationEntryManager notificationEntryManager,
                 Lazy<ShadeController> shadeController,
                 StatusBarStateController statusBarStateController,
-                Handler mainHandler) {
+                Handler mainHandler,
+                RemoteInputUriController remoteInputUriController) {
             super(context, lockscreenUserManager, smartReplyController, notificationEntryManager,
-                    shadeController, statusBarStateController, mainHandler);
+                    shadeController, statusBarStateController, mainHandler,
+                    remoteInputUriController);
         }
 
         public void setUpWithPresenterForTest(Callback callback,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index 88fb3e1..95ce53c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -42,6 +42,7 @@
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.phone.ShadeController;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -72,6 +73,7 @@
     @Mock private NotificationEntryManager mNotificationEntryManager;
     @Mock private IStatusBarService mIStatusBarService;
     @Mock private StatusBarStateController mStatusBarStateController;
+    @Mock private RemoteInputUriController mRemoteInputUriController;
 
     @Before
     public void setUp() {
@@ -88,7 +90,8 @@
                 mock(NotificationLockscreenUserManager.class), mSmartReplyController,
                 mNotificationEntryManager, () -> mock(ShadeController.class),
                 mStatusBarStateController,
-                Handler.createAsync(Looper.myLooper()));
+                Handler.createAsync(Looper.myLooper()),
+                mRemoteInputUriController);
         mRemoteInputManager.setUpWithCallback(mCallback, mDelegate);
         mNotification = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 5d15205..c21e3ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -131,6 +131,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.util.InjectionInflationController;
@@ -181,6 +182,7 @@
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
     @Mock private RemoteInputController mRemoteInputController;
+    @Mock private RemoteInputUriController mRemoteInputUriController;
     @Mock private StatusBarStateControllerImpl mStatusBarStateController;
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
@@ -369,7 +371,8 @@
                 mPowerManager,
                 mDozeScrimController,
                 mCommandQueue,
-                mPluginManager);
+                mPluginManager,
+                mRemoteInputUriController);
 
         when(mStatusBarWindowView.findViewById(R.id.lock_icon_container)).thenReturn(
                 mLockIconContainer);
diff --git a/services/core/java/com/android/server/notification/InlineReplyUriRecord.java b/services/core/java/com/android/server/notification/InlineReplyUriRecord.java
new file mode 100644
index 0000000..76cfb03
--- /dev/null
+++ b/services/core/java/com/android/server/notification/InlineReplyUriRecord.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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 android.net.Uri;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+/**
+ * A record of inline reply (ex. RemoteInput) URI grants associated with a Notification.
+ */
+public final class InlineReplyUriRecord {
+    private final IBinder mPermissionOwner;
+    private final ArraySet<Uri> mUris;
+    private final UserHandle mUser;
+    private final String mPackageName;
+    private final String mKey;
+
+    /**
+     * Construct a new InlineReplyUriRecord.
+     * @param owner The PermissionOwner associated with this record.
+     * @param user The user associated with this record.
+     * @param packageName The name of the package which posted the notification.
+     * @param key The key of the original NotificationRecord this notification as created with.
+     */
+    public InlineReplyUriRecord(IBinder owner, UserHandle user, String packageName, String key) {
+        mPermissionOwner = owner;
+        mUris = new ArraySet<>();
+        mUser = user;
+        mPackageName = packageName;
+        mKey = key;
+    }
+
+    /**
+     * Get the permission owner associated with this record.
+     */
+    public IBinder getPermissionOwner() {
+        return mPermissionOwner;
+    }
+
+    /**
+     * Get the content URIs associated with this record.
+     */
+    public ArraySet<Uri> getUris() {
+        return mUris;
+    }
+
+    /**
+     * Associate a new content URI with this record.
+     */
+    public void addUri(Uri uri) {
+        mUris.add(uri);
+    }
+
+    /**
+     * Get the user id associated with this record.
+     * If the UserHandle associated with this record belongs to USER_ALL, return the ID for
+     * USER_SYSTEM instead, to avoid errors around modifying URI permissions for an invalid user ID.
+     */
+    public int getUserId() {
+        int userId = mUser.getIdentifier();
+        if (userId == UserHandle.USER_ALL) {
+            return UserHandle.USER_SYSTEM;
+        } else {
+            return userId;
+        }
+    }
+
+    /**
+     * Get the name of the package associated with this record.
+     */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Get the key associated with this record.
+     */
+    public String getKey() {
+        return mKey;
+    }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 6f0ad33..88fc072 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -18,6 +18,7 @@
 
 import android.app.Notification;
 import android.net.Uri;
+import android.os.UserHandle;
 import android.service.notification.NotificationStats;
 
 import com.android.internal.statusbar.NotificationVisibility;
@@ -53,7 +54,13 @@
      * Grant permission to read the specified URI to the package associated with the
      * NotificationRecord associated with the given key.
      */
-    void grantInlineReplyUriPermission(String key, Uri uri, int callingUid);
+    void grantInlineReplyUriPermission(String key, Uri uri, UserHandle user, String packageName,
+            int callingUid);
+
+    /**
+     * Clear inline URI grants associated with the given notification.
+     */
+    void clearInlineReplyUriPermissions(String key, int callingUid);
 
     /**
      * Notifies that smart replies and actions have been added to the UI.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0fc1718..5e76401 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -415,6 +415,8 @@
     @GuardedBy("mNotificationLock")
     final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();
     @GuardedBy("mNotificationLock")
+    final ArrayMap<String, InlineReplyUriRecord> mInlineReplyRecordsByKey = new ArrayMap<>();
+    @GuardedBy("mNotificationLock")
     final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();
     @GuardedBy("mNotificationLock")
     final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
@@ -1167,42 +1169,53 @@
          * user associated with the NotificationRecord, and this grant will fail when trying
          * to grant URI permissions across users.
          */
-        public void grantInlineReplyUriPermission(String key, Uri uri, int callingUid) {
+        public void grantInlineReplyUriPermission(String key, Uri uri, UserHandle user,
+                String packageName, int callingUid) {
             synchronized (mNotificationLock) {
-                NotificationRecord r = mNotificationsByKey.get(key);
-                if (r != null) {
-                    IBinder owner = r.permissionOwner;
-                    if (owner == null) {
-                        r.permissionOwner = mUgmInternal.newUriPermissionOwner("NOTIF:" + key);
-                        owner = r.permissionOwner;
-                    }
-                    int uid = callingUid;
-                    int userId = r.sbn.getUserId();
-                    if (userId == UserHandle.USER_ALL) {
-                        userId = USER_SYSTEM;
-                    }
-                    if (UserHandle.getUserId(uid) != userId) {
-                        try {
-                            final String[] pkgs = mPackageManager.getPackagesForUid(callingUid);
-                            if (pkgs == null) {
-                                Log.e(TAG, "Cannot grant uri permission to unknown UID: "
-                                        + callingUid);
-                            }
-                            final String pkg = pkgs[0]; // Get the SystemUI package
-                            // Find the UID for SystemUI for the correct user
-                            uid =  mPackageManager.getPackageUid(pkg, 0, userId);
-                        } catch (RemoteException re) {
-                            Log.e(TAG, "Cannot talk to package manager", re);
+                InlineReplyUriRecord r = mInlineReplyRecordsByKey.get(key);
+                if (r == null) {
+                    InlineReplyUriRecord newRecord = new InlineReplyUriRecord(
+                            mUgmInternal.newUriPermissionOwner("INLINE_REPLY:" + key),
+                            user,
+                            packageName,
+                            key);
+                    r = newRecord;
+                    mInlineReplyRecordsByKey.put(key, r);
+                }
+                IBinder owner = r.getPermissionOwner();
+                int uid = callingUid;
+                int userId = r.getUserId();
+                if (UserHandle.getUserId(uid) != userId) {
+                    try {
+                        final String[] pkgs = mPackageManager.getPackagesForUid(callingUid);
+                        if (pkgs == null) {
+                            Log.e(TAG, "Cannot grant uri permission to unknown UID: "
+                                    + callingUid);
                         }
+                        final String pkg = pkgs[0]; // Get the SystemUI package
+                        // Find the UID for SystemUI for the correct user
+                        uid =  mPackageManager.getPackageUid(pkg, 0, userId);
+                    } catch (RemoteException re) {
+                        Log.e(TAG, "Cannot talk to package manager", re);
                     }
-                    grantUriPermission(owner, uri, uid, r.sbn.getPackageName(), userId);
-                } else {
-                    Log.w(TAG, "No record found for notification key:" + key);
+                }
+                r.addUri(uri);
+                grantUriPermission(owner, uri, uid, r.getPackageName(), userId);
+            }
+        }
 
-                    // TODO: figure out cancel story. I think it's: sysui needs to tell us
-                    // whenever noitifications held by a lifetimextender go away
-                    // IBinder owner = mUgmInternal.newUriPermissionOwner("InlineReply:" + key);
-                    // pass in userId and package as well as key (key for logging purposes)
+        @Override
+        /**
+         * Clears inline URI permission grants by destroying the permission owner for the specified
+         * notification.
+         */
+        public void clearInlineReplyUriPermissions(String key, int callingUid) {
+            synchronized (mNotificationLock) {
+                InlineReplyUriRecord uriRecord = mInlineReplyRecordsByKey.get(key);
+                if (uriRecord != null) {
+                    destroyPermissionOwner(uriRecord.getPermissionOwner(), uriRecord.getUserId(),
+                            "INLINE_REPLY: " + uriRecord.getKey());
+                    mInlineReplyRecordsByKey.remove(key);
                 }
             }
         }
@@ -7036,15 +7049,8 @@
 
         // If we have no Uris to grant, but an existing owner, go destroy it
         if (newUris == null && permissionOwner != null) {
-            final long ident = Binder.clearCallingIdentity();
-            try {
-                if (DBG) Slog.d(TAG, key + ": destroying owner");
-                mUgmInternal.revokeUriPermissionFromOwner(permissionOwner, null, ~0,
-                        UserHandle.getUserId(oldRecord.getUid()));
-                permissionOwner = null;
-            } finally {
-                Binder.restoreCallingIdentity(ident);
-            }
+            destroyPermissionOwner(permissionOwner, UserHandle.getUserId(oldRecord.getUid()), key);
+            permissionOwner = null;
         }
 
         // Grant access to new Uris
@@ -7065,7 +7071,9 @@
                 final Uri uri = oldUris.valueAt(i);
                 if (newUris == null || !newUris.contains(uri)) {
                     if (DBG) Slog.d(TAG, key + ": revoking " + uri);
-                    revokeUriPermission(permissionOwner, uri, oldRecord.getUid());
+                    int userId = ContentProvider.getUserIdFromUri(
+                            uri, UserHandle.getUserId(oldRecord.getUid()));
+                    revokeUriPermission(permissionOwner, uri, userId);
                 }
             }
         }
@@ -7092,7 +7100,7 @@
         }
     }
 
-    private void revokeUriPermission(IBinder owner, Uri uri, int sourceUid) {
+    private void revokeUriPermission(IBinder owner, Uri uri, int userId) {
         if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
 
         final long ident = Binder.clearCallingIdentity();
@@ -7101,7 +7109,17 @@
                     owner,
                     ContentProvider.getUriWithoutUserId(uri),
                     Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
+                    userId);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void destroyPermissionOwner(IBinder owner, int userId, String logKey) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            if (DBG) Slog.d(TAG, logKey + ": destroying owner");
+            mUgmInternal.revokeUriPermissionFromOwner(owner, null, ~0, userId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 489c343..effeb80 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1364,12 +1364,26 @@
     }
 
     @Override
-    public void grantInlineReplyUriPermission(String key, Uri uri) {
+    public void grantInlineReplyUriPermission(String key, Uri uri, UserHandle user,
+            String packageName) {
         enforceStatusBarService();
         int callingUid = Binder.getCallingUid();
         long identity = Binder.clearCallingIdentity();
         try {
-            mNotificationDelegate.grantInlineReplyUriPermission(key, uri, callingUid);
+            mNotificationDelegate.grantInlineReplyUriPermission(key, uri, user, packageName,
+                    callingUid);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public void clearInlineReplyUriPermissions(String key) {
+        enforceStatusBarService();
+        int callingUid = Binder.getCallingUid();
+        long identity = Binder.clearCallingIdentity();
+        try {
+            mNotificationDelegate.clearInlineReplyUriPermissions(key, callingUid);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index cd0f4f1..1ee71fb 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -5478,7 +5478,7 @@
         Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
 
         mService.mNotificationDelegate.grantInlineReplyUriPermission(
-                nr.getKey(), uri, nr.sbn.getUid());
+                nr.getKey(), uri, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
 
         // Grant permission called for the UID of SystemUI under the target user ID
         verify(mUgm, times(1)).grantUriPermissionFromOwner(any(),
@@ -5487,6 +5487,27 @@
     }
 
     @Test
+    public void testGrantInlineReplyUriPermission_noRecordExists() throws Exception {
+        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, 0);
+        waitForIdle();
+
+        // No notifications exist for the given record
+        StatusBarNotification[] notifsBefore = mBinderService.getActiveNotifications(PKG);
+        assertEquals(0, notifsBefore.length);
+
+        Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
+        int uid = 0; // sysui on primary user
+
+        mService.mNotificationDelegate.grantInlineReplyUriPermission(
+                nr.getKey(), uri, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
+
+        // Grant permission still called if no NotificationRecord exists for the given key
+        verify(mUgm, times(1)).grantUriPermissionFromOwner(any(),
+                eq(nr.sbn.getUid()), eq(nr.sbn.getPackageName()), eq(uri), anyInt(), anyInt(),
+                eq(nr.sbn.getUserId()));
+    }
+
+    @Test
     public void testGrantInlineReplyUriPermission_userAll() throws Exception {
         // generate a NotificationRecord for USER_ALL to make sure it's converted into USER_SYSTEM
         NotificationRecord nr =
@@ -5504,7 +5525,7 @@
         Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
 
         mService.mNotificationDelegate.grantInlineReplyUriPermission(
-                nr.getKey(), uri, nr.sbn.getUid());
+                nr.getKey(), uri, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
 
         // Target user for the grant is USER_ALL instead of USER_SYSTEM
         verify(mUgm, times(1)).grantUriPermissionFromOwner(any(),
@@ -5531,7 +5552,7 @@
         Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
 
         int uid = 0; // sysui on primary user
-        int otherUserUid = (otherUserId * 100000) + 1; // SystemUI as a different user
+        int otherUserUid = (otherUserId * 100000) + 1; // sysui as a different user
         String sysuiPackage = "sysui";
         final String[] sysuiPackages = new String[] { sysuiPackage };
         when(mPackageManager.getPackagesForUid(uid)).thenReturn(sysuiPackages);
@@ -5541,7 +5562,8 @@
         when(mPackageManager.getPackageUid(sysuiPackage, 0, otherUserId))
                 .thenReturn(otherUserUid);
 
-        mService.mNotificationDelegate.grantInlineReplyUriPermission(nr.getKey(), uri, uid);
+        mService.mNotificationDelegate.grantInlineReplyUriPermission(
+                nr.getKey(), uri, nr.sbn.getUser(), nr.sbn.getPackageName(), uid);
 
         // Target user for the grant is USER_ALL instead of USER_SYSTEM
         verify(mUgm, times(1)).grantUriPermissionFromOwner(any(),
@@ -5550,22 +5572,64 @@
     }
 
     @Test
-    public void testGrantInlineReplyUriPermission_noRecordExists() throws Exception {
-        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel);
-        waitForIdle();
+    public void testClearInlineReplyUriPermission_uriRecordExists() throws Exception {
+        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, 0);
+        reset(mPackageManager);
 
-        // No notifications exist for the given record
-        StatusBarNotification[] notifsBefore = mBinderService.getActiveNotifications(PKG);
-        assertEquals(0, notifsBefore.length);
+        Uri uri1 = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
+        Uri uri2 = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 2);
 
-        Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
-        int uid = 0; // sysui on primary user
+        // create an inline record with two uris in it
+        mService.mNotificationDelegate.grantInlineReplyUriPermission(
+                nr.getKey(), uri1, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
+        mService.mNotificationDelegate.grantInlineReplyUriPermission(
+                nr.getKey(), uri2, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
 
-        mService.mNotificationDelegate.grantInlineReplyUriPermission(nr.getKey(), uri, uid);
+        InlineReplyUriRecord record = mService.mInlineReplyRecordsByKey.get(nr.getKey());
+        assertNotNull(record); // record exists
+        assertEquals(record.getUris().size(), 2); // record has two uris in it
 
-        // Grant permission not called if no record exists for the given key
-        verify(mUgm, times(0)).grantUriPermissionFromOwner(any(), anyInt(), any(),
-                eq(uri), anyInt(), anyInt(), anyInt());
+        mService.mNotificationDelegate.clearInlineReplyUriPermissions(nr.getKey(), nr.sbn.getUid());
+
+        // permissionOwner destroyed
+        verify(mUgmInternal, times(1)).revokeUriPermissionFromOwner(
+                eq(record.getPermissionOwner()), eq(null), eq(~0), eq(nr.getUserId()));
+    }
+
+
+    @Test
+    public void testClearInlineReplyUriPermission_noUriRecordExists() throws Exception {
+        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, 0);
+        reset(mPackageManager);
+
+        mService.mNotificationDelegate.clearInlineReplyUriPermissions(nr.getKey(), nr.sbn.getUid());
+
+        // no permissionOwner destroyed
+        verify(mUgmInternal, times(0)).revokeUriPermissionFromOwner(
+                any(), eq(null), eq(~0), eq(nr.getUserId()));
+    }
+
+    @Test
+    public void testClearInlineReplyUriPermission_userAll() throws Exception {
+        NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel,
+                UserHandle.USER_ALL);
+        reset(mPackageManager);
+
+        Uri uri1 = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1);
+        Uri uri2 = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 2);
+
+        // create an inline record a uri in it
+        mService.mNotificationDelegate.grantInlineReplyUriPermission(
+                nr.getKey(), uri1, nr.sbn.getUser(), nr.sbn.getPackageName(), nr.sbn.getUid());
+
+        InlineReplyUriRecord record = mService.mInlineReplyRecordsByKey.get(nr.getKey());
+        assertNotNull(record); // record exists
+
+        mService.mNotificationDelegate.clearInlineReplyUriPermissions(nr.getKey(), nr.sbn.getUid());
+
+        // permissionOwner destroyed for USER_SYSTEM, not USER_ALL
+        verify(mUgmInternal, times(1)).revokeUriPermissionFromOwner(
+                eq(record.getPermissionOwner()), eq(null), eq(~0), eq(USER_SYSTEM));
     }
 
     @Test