1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.server.slice;
16 
17 import android.app.slice.SliceProvider;
18 import android.app.slice.SliceSpec;
19 import android.content.ContentProviderClient;
20 import android.net.Uri;
21 import android.os.Binder;
22 import android.os.Bundle;
23 import android.os.IBinder;
24 import android.os.IBinder.DeathRecipient;
25 import android.os.RemoteException;
26 import android.util.ArrayMap;
27 import android.util.ArraySet;
28 import android.util.Log;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import java.util.Arrays;
34 import java.util.List;
35 import java.util.Objects;
36 
37 /**
38  * Manages the state of a pinned slice.
39  */
40 public class PinnedSliceState {
41 
42     private static final long SLICE_TIMEOUT = 5000;
43     private static final String TAG = "PinnedSliceState";
44 
45     private final Object mLock;
46 
47     private final SliceManagerService mService;
48     private final Uri mUri;
49     @GuardedBy("mLock")
50     private final ArraySet<String> mPinnedPkgs = new ArraySet<>();
51     @GuardedBy("mLock")
52     private final ArrayMap<IBinder, ListenerInfo> mListeners = new ArrayMap<>();
53     private final String mPkg;
54     @GuardedBy("mLock")
55     private SliceSpec[] mSupportedSpecs = null;
56 
57     private final DeathRecipient mDeathRecipient = this::handleRecheckListeners;
58     private boolean mSlicePinned;
59 
PinnedSliceState(SliceManagerService service, Uri uri, String pkg)60     public PinnedSliceState(SliceManagerService service, Uri uri, String pkg) {
61         mService = service;
62         mUri = uri;
63         mPkg = pkg;
64         mLock = mService.getLock();
65     }
66 
getPkg()67     public String getPkg() {
68         return mPkg;
69     }
70 
getSpecs()71     public SliceSpec[] getSpecs() {
72         return mSupportedSpecs;
73     }
74 
mergeSpecs(SliceSpec[] supportedSpecs)75     public void mergeSpecs(SliceSpec[] supportedSpecs) {
76         synchronized (mLock) {
77             if (mSupportedSpecs == null) {
78                 mSupportedSpecs = supportedSpecs;
79             } else {
80                 List<SliceSpec> specs = Arrays.asList(mSupportedSpecs);
81                 mSupportedSpecs = specs.stream().map(s -> {
82                     SliceSpec other = findSpec(supportedSpecs, s.getType());
83                     if (other == null) return null;
84                     if (other.getRevision() < s.getRevision()) {
85                         return other;
86                     }
87                     return s;
88                 }).filter(s -> s != null).toArray(SliceSpec[]::new);
89             }
90         }
91     }
92 
findSpec(SliceSpec[] specs, String type)93     private SliceSpec findSpec(SliceSpec[] specs, String type) {
94         for (SliceSpec spec : specs) {
95             if (Objects.equals(spec.getType(), type)) {
96                 return spec;
97             }
98         }
99         return null;
100     }
101 
getUri()102     public Uri getUri() {
103         return mUri;
104     }
105 
destroy()106     public void destroy() {
107         setSlicePinned(false);
108     }
109 
setSlicePinned(boolean pinned)110     private void setSlicePinned(boolean pinned) {
111         synchronized (mLock) {
112             if (mSlicePinned == pinned) return;
113             mSlicePinned = pinned;
114             if (pinned) {
115                 mService.getHandler().post(this::handleSendPinned);
116             } else {
117                 mService.getHandler().post(this::handleSendUnpinned);
118             }
119         }
120     }
121 
pin(String pkg, SliceSpec[] specs, IBinder token)122     public void pin(String pkg, SliceSpec[] specs, IBinder token) {
123         synchronized (mLock) {
124             mListeners.put(token, new ListenerInfo(token, pkg, true,
125                     Binder.getCallingUid(), Binder.getCallingPid()));
126             try {
127                 token.linkToDeath(mDeathRecipient, 0);
128             } catch (RemoteException e) {
129             }
130             mergeSpecs(specs);
131             setSlicePinned(true);
132         }
133     }
134 
unpin(String pkg, IBinder token)135     public boolean unpin(String pkg, IBinder token) {
136         synchronized (mLock) {
137             token.unlinkToDeath(mDeathRecipient, 0);
138             mListeners.remove(token);
139         }
140         return !hasPinOrListener();
141     }
142 
isListening()143     public boolean isListening() {
144         synchronized (mLock) {
145             return !mListeners.isEmpty();
146         }
147     }
148 
149     @VisibleForTesting
hasPinOrListener()150     public boolean hasPinOrListener() {
151         synchronized (mLock) {
152             return !mPinnedPkgs.isEmpty() || !mListeners.isEmpty();
153         }
154     }
155 
getClient()156     ContentProviderClient getClient() {
157         ContentProviderClient client = mService.getContext().getContentResolver()
158                 .acquireUnstableContentProviderClient(mUri);
159         if (client == null) return null;
160         client.setDetectNotResponding(SLICE_TIMEOUT);
161         return client;
162     }
163 
checkSelfRemove()164     private void checkSelfRemove() {
165         if (!hasPinOrListener()) {
166             // All the listeners died, remove from pinned state.
167             mService.removePinnedSlice(mUri);
168         }
169     }
170 
handleRecheckListeners()171     private void handleRecheckListeners() {
172         if (!hasPinOrListener()) return;
173         synchronized (mLock) {
174             for (int i = mListeners.size() - 1; i >= 0; i--) {
175                 ListenerInfo l = mListeners.valueAt(i);
176                 if (!l.token.isBinderAlive()) {
177                     mListeners.removeAt(i);
178                 }
179             }
180             checkSelfRemove();
181         }
182     }
183 
handleSendPinned()184     private void handleSendPinned() {
185         try (ContentProviderClient client = getClient()) {
186             if (client == null) return;
187             Bundle b = new Bundle();
188             b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
189             try {
190                 client.call(SliceProvider.METHOD_PIN, null, b);
191             } catch (Exception e) {
192                 Log.w(TAG, "Unable to contact " + mUri, e);
193             }
194         }
195     }
196 
handleSendUnpinned()197     private void handleSendUnpinned() {
198         try (ContentProviderClient client = getClient()) {
199             if (client == null) return;
200             Bundle b = new Bundle();
201             b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
202             try {
203                 client.call(SliceProvider.METHOD_UNPIN, null, b);
204             } catch (Exception e) {
205                 Log.w(TAG, "Unable to contact " + mUri, e);
206             }
207         }
208     }
209 
210     private class ListenerInfo {
211 
212         private IBinder token;
213         private String pkg;
214         private boolean hasPermission;
215         private int callingUid;
216         private int callingPid;
217 
ListenerInfo(IBinder token, String pkg, boolean hasPermission, int callingUid, int callingPid)218         public ListenerInfo(IBinder token, String pkg, boolean hasPermission,
219                 int callingUid, int callingPid) {
220             this.token = token;
221             this.pkg = pkg;
222             this.hasPermission = hasPermission;
223             this.callingUid = callingUid;
224             this.callingPid = callingPid;
225         }
226     }
227 }
228