1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.print;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.ServiceConnection;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.ResolveInfo;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 import android.os.UserHandle;
30 import android.printservice.recommendation.IRecommendationService;
31 import android.printservice.recommendation.IRecommendationServiceCallbacks;
32 import android.printservice.recommendation.RecommendationInfo;
33 import android.util.Log;
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.util.Preconditions;
36 
37 import java.util.List;
38 
39 import static android.content.pm.PackageManager.GET_META_DATA;
40 import static android.content.pm.PackageManager.GET_SERVICES;
41 import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
42 
43 /**
44  * Connection to a remote print service recommendation service.
45  */
46 class RemotePrintServiceRecommendationService {
47     private static final String LOG_TAG = "RemotePrintServiceRecS";
48 
49     /** Lock for this object */
50     private final Object mLock = new Object();
51 
52     /** Context used for the connection */
53     private @NonNull final Context mContext;
54 
55     /**  The connection to the service (if {@link #mIsBound bound}) */
56     @GuardedBy("mLock")
57     private @NonNull final Connection mConnection;
58 
59     /** If the service is currently bound. */
60     @GuardedBy("mLock")
61     private boolean mIsBound;
62 
63     /** The service once bound */
64     @GuardedBy("mLock")
65     private IRecommendationService mService;
66 
67     /**
68      * Callbacks to be called when there are updates to the print service recommendations.
69      */
70     public interface RemotePrintServiceRecommendationServiceCallbacks {
71         /**
72          * Called when there is an update list of print service recommendations.
73          *
74          * @param recommendations The new recommendations.
75          */
onPrintServiceRecommendationsUpdated( @ullable List<RecommendationInfo> recommendations)76         void onPrintServiceRecommendationsUpdated(
77                 @Nullable List<RecommendationInfo> recommendations);
78     }
79 
80     /**
81      * @return The intent that is used to connect to the print service recommendation service.
82      */
getServiceIntent(@onNull UserHandle userHandle)83     private Intent getServiceIntent(@NonNull UserHandle userHandle) throws Exception {
84         List<ResolveInfo> installedServices = mContext.getPackageManager()
85                 .queryIntentServicesAsUser(new Intent(
86                         android.printservice.recommendation.RecommendationService.SERVICE_INTERFACE),
87                         GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
88                         userHandle.getIdentifier());
89 
90         if (installedServices.size() != 1) {
91             throw new Exception(installedServices.size() + " instead of exactly one service found");
92         }
93 
94         ResolveInfo installedService = installedServices.get(0);
95 
96         ComponentName serviceName = new ComponentName(
97                 installedService.serviceInfo.packageName,
98                 installedService.serviceInfo.name);
99 
100         ApplicationInfo appInfo = mContext.getPackageManager()
101                 .getApplicationInfo(installedService.serviceInfo.packageName, 0);
102 
103         if (appInfo == null) {
104             throw new Exception("Cannot read appInfo for service");
105         }
106 
107         if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
108             throw new Exception("Service is not part of the system");
109         }
110 
111         if (!android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE.equals(
112                 installedService.serviceInfo.permission)) {
113             throw new Exception("Service " + serviceName.flattenToShortString()
114                     + " does not require permission "
115                     + android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE);
116         }
117 
118         Intent serviceIntent = new Intent();
119         serviceIntent.setComponent(serviceName);
120 
121         return serviceIntent;
122     }
123 
124     /**
125      * Open a new connection to a {@link IRecommendationService remote print service
126      * recommendation service}.
127      *
128      * @param context    The context establishing the connection
129      * @param userHandle The user the connection is for
130      * @param callbacks  The callbacks to call by the service
131      */
RemotePrintServiceRecommendationService(@onNull Context context, @NonNull UserHandle userHandle, @NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks)132     RemotePrintServiceRecommendationService(@NonNull Context context,
133             @NonNull UserHandle userHandle,
134             @NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
135         mContext = context;
136         mConnection = new Connection(callbacks);
137 
138         try {
139             Intent serviceIntent = getServiceIntent(userHandle);
140 
141             synchronized (mLock) {
142                 mIsBound = mContext.bindServiceAsUser(serviceIntent, mConnection,
143                         Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, userHandle);
144 
145                 if (!mIsBound) {
146                     throw new Exception("Failed to bind to service " + serviceIntent);
147                 }
148             }
149         } catch (Exception e) {
150             Log.e(LOG_TAG, "Could not connect to print service recommendation service", e);
151         }
152     }
153 
154     /**
155      * Terminate the connection to the {@link IRecommendationService remote print
156      * service recommendation service}.
157      */
close()158     void close() {
159         synchronized (mLock) {
160             if (mService != null) {
161                 try {
162                     mService.registerCallbacks(null);
163                 } catch (RemoteException e) {
164                     Log.e(LOG_TAG, "Could not unregister callbacks", e);
165                 }
166 
167                 mService = null;
168             }
169 
170             if (mIsBound) {
171                 mContext.unbindService(mConnection);
172                 mIsBound = false;
173             }
174         }
175     }
176 
177     @Override
finalize()178     protected void finalize() throws Throwable {
179         if (mIsBound || mService != null) {
180             Log.w(LOG_TAG, "Service still connected on finalize()");
181             close();
182         }
183 
184         super.finalize();
185     }
186 
187     /**
188      * Connection to the service.
189      */
190     private class Connection implements ServiceConnection {
191         private final RemotePrintServiceRecommendationServiceCallbacks mCallbacks;
192 
Connection(@onNull RemotePrintServiceRecommendationServiceCallbacks callbacks)193         public Connection(@NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
194             mCallbacks = callbacks;
195         }
196 
197         @Override
onServiceConnected(ComponentName name, IBinder service)198         public void onServiceConnected(ComponentName name, IBinder service) {
199             synchronized (mLock) {
200                 mService = (IRecommendationService)IRecommendationService.Stub.asInterface(service);
201 
202                 try {
203                     mService.registerCallbacks(new IRecommendationServiceCallbacks.Stub() {
204                         @Override
205                         public void onRecommendationsUpdated(
206                                 List<RecommendationInfo> recommendations) {
207                             synchronized (mLock) {
208                                 if (mIsBound && mService != null) {
209                                     if (recommendations != null) {
210                                         Preconditions.checkCollectionElementsNotNull(
211                                                 recommendations, "recommendation");
212                                     }
213 
214                                     mCallbacks.onPrintServiceRecommendationsUpdated(
215                                             recommendations);
216                                 }
217                             }
218                         }
219                     });
220                 } catch (RemoteException e) {
221                     Log.e(LOG_TAG, "Could not register callbacks", e);
222                 }
223             }
224         }
225 
226         @Override
onServiceDisconnected(ComponentName name)227         public void onServiceDisconnected(ComponentName name) {
228             Log.w(LOG_TAG, "Unexpected termination of connection");
229 
230             synchronized (mLock) {
231                 mService = null;
232             }
233         }
234     }
235 }
236