1 /*
2  * Copyright (C) 2018 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.internal.telephony.ims;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.os.IBinder;
24 import android.telephony.ims.aidl.IImsServiceController;
25 import android.telephony.ims.stub.ImsFeatureConfiguration;
26 import android.util.Log;
27 
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.Set;
32 
33 /**
34  * Manages the querying of multiple ImsServices asynchronously in order to retrieve the ImsFeatures
35  * they support.
36  */
37 
38 public class ImsServiceFeatureQueryManager {
39 
40     private final class ImsServiceFeatureQuery implements ServiceConnection {
41 
42         private static final String LOG_TAG = "ImsServiceFeatureQuery";
43 
44         private final ComponentName mName;
45         private final String mIntentFilter;
46         // Track the status of whether or not the Service has died in case we need to permanently
47         // unbind (see onNullBinding below).
48         private boolean mIsServiceConnectionDead = false;
49 
50 
ImsServiceFeatureQuery(ComponentName name, String intentFilter)51         ImsServiceFeatureQuery(ComponentName name, String intentFilter) {
52             mName = name;
53             mIntentFilter = intentFilter;
54         }
55 
56         /**
57          * Starts the bind to the ImsService specified ComponentName.
58          * @return true if binding started, false if it failed and will not recover.
59          */
start()60         public boolean start() {
61             Log.d(LOG_TAG, "start: intent filter=" + mIntentFilter + ", name=" + mName);
62             Intent imsServiceIntent = new Intent(mIntentFilter).setComponent(mName);
63             int serviceFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
64                     | Context.BIND_IMPORTANT;
65             boolean bindStarted = mContext.bindService(imsServiceIntent, this, serviceFlags);
66             if (!bindStarted) {
67                 // Docs say to unbind if this fails.
68                 cleanup();
69             }
70             return bindStarted;
71         }
72 
73         @Override
onServiceConnected(ComponentName name, IBinder service)74         public void onServiceConnected(ComponentName name, IBinder service) {
75             Log.i(LOG_TAG, "onServiceConnected for component: " + name);
76             if (service != null) {
77                 queryImsFeatures(IImsServiceController.Stub.asInterface(service));
78             } else {
79                 Log.w(LOG_TAG, "onServiceConnected: " + name + " binder null.");
80                 cleanup();
81                 mListener.onPermanentError(name);
82             }
83         }
84 
85         @Override
onServiceDisconnected(ComponentName name)86         public void onServiceDisconnected(ComponentName name) {
87             Log.w(LOG_TAG, "onServiceDisconnected for component: " + name);
88         }
89 
90         @Override
onBindingDied(ComponentName name)91         public void onBindingDied(ComponentName name) {
92             mIsServiceConnectionDead = true;
93             Log.w(LOG_TAG, "onBindingDied: " + name);
94             cleanup();
95             // retry again!
96             mListener.onError(name);
97         }
98 
99         @Override
onNullBinding(ComponentName name)100         public void onNullBinding(ComponentName name) {
101             Log.w(LOG_TAG, "onNullBinding: " + name);
102             // onNullBinding will happen after onBindingDied. In this case, we should not
103             // permanently unbind and instead let the automatic rebind occur.
104             if (mIsServiceConnectionDead) return;
105             cleanup();
106             mListener.onPermanentError(name);
107         }
108 
queryImsFeatures(IImsServiceController controller)109         private void queryImsFeatures(IImsServiceController controller) {
110             ImsFeatureConfiguration config;
111             try {
112                 config = controller.querySupportedImsFeatures();
113             } catch (Exception e) {
114                 Log.w(LOG_TAG, "queryImsFeatures - error: " + e);
115                 cleanup();
116                 // Retry again!
117                 mListener.onError(mName);
118                 return;
119             }
120             Set<ImsFeatureConfiguration.FeatureSlotPair> servicePairs;
121             if (config == null) {
122                 // ensure that if the ImsService sent a null config, we return an empty feature
123                 // set to the ImsResolver.
124                 servicePairs = Collections.emptySet();
125             } else {
126                 servicePairs = config.getServiceFeatures();
127             }
128             // Complete, remove from active queries and notify.
129             cleanup();
130             mListener.onComplete(mName, servicePairs);
131         }
132 
cleanup()133         private void cleanup() {
134             mContext.unbindService(this);
135             synchronized (mLock) {
136                 mActiveQueries.remove(mName);
137             }
138         }
139     }
140 
141     public interface Listener {
142         /**
143          * Called when a query has completed.
144          * @param name The Package Name of the query
145          * @param features A Set of slotid->feature pairs that the ImsService supports.
146          */
onComplete(ComponentName name, Set<ImsFeatureConfiguration.FeatureSlotPair> features)147         void onComplete(ComponentName name, Set<ImsFeatureConfiguration.FeatureSlotPair> features);
148 
149         /**
150          * Called when a query has failed and should be retried.
151          */
onError(ComponentName name)152         void onError(ComponentName name);
153 
154         /**
155          * Called when a query has failed due to a permanent error and should not be retried.
156          */
onPermanentError(ComponentName name)157         void onPermanentError(ComponentName name);
158     }
159 
160     // Maps an active ImsService query (by Package Name String) its query.
161     private final Map<ComponentName, ImsServiceFeatureQuery> mActiveQueries = new HashMap<>();
162     private final Context mContext;
163     private final Listener mListener;
164     private final Object mLock = new Object();
165 
ImsServiceFeatureQueryManager(Context context, Listener listener)166     public ImsServiceFeatureQueryManager(Context context, Listener listener) {
167         mContext = context;
168         mListener = listener;
169     }
170 
171     /**
172      * Starts an ImsService feature query for the ComponentName and Intent specified.
173      * @param name The ComponentName of the ImsService being queried.
174      * @param intentFilter The Intent filter that the ImsService specified.
175      * @return true if the query started, false if it was unable to start.
176      */
startQuery(ComponentName name, String intentFilter)177     public boolean startQuery(ComponentName name, String intentFilter) {
178         synchronized (mLock) {
179             if (mActiveQueries.containsKey(name)) {
180                 // We already have an active query, wait for it to return.
181                 return true;
182             }
183             ImsServiceFeatureQuery query = new ImsServiceFeatureQuery(name, intentFilter);
184             mActiveQueries.put(name, query);
185             return query.start();
186         }
187     }
188 
189     /**
190      * @return true if there are any active queries, false if the manager is idle.
191      */
isQueryInProgress()192     public boolean isQueryInProgress() {
193         synchronized (mLock) {
194             return !mActiveQueries.isEmpty();
195         }
196     }
197 }
198