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
17package com.android.server.print;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.ServiceConnection;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.ResolveInfo;
27import android.os.IBinder;
28import android.os.RemoteException;
29import android.os.UserHandle;
30import android.printservice.recommendation.IRecommendationService;
31import android.printservice.recommendation.IRecommendationServiceCallbacks;
32import android.printservice.recommendation.RecommendationInfo;
33import android.util.Log;
34import com.android.internal.annotations.GuardedBy;
35import com.android.internal.util.Preconditions;
36
37import java.util.List;
38
39import static android.content.pm.PackageManager.GET_META_DATA;
40import static android.content.pm.PackageManager.GET_SERVICES;
41import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
42
43/**
44 * Connection to a remote print service recommendation service.
45 */
46class 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         */
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     */
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     */
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     */
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
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
193        public Connection(@NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
194            mCallbacks = callbacks;
195        }
196
197        @Override
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
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