1/*
2 * Copyright (C) 2017 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 android.telephony.ims.feature;
18
19import android.annotation.IntDef;
20import android.content.Context;
21import android.content.Intent;
22import android.os.RemoteException;
23import android.telephony.SubscriptionManager;
24import android.util.Log;
25
26import com.android.ims.internal.IImsFeatureStatusCallback;
27
28import java.lang.annotation.Retention;
29import java.lang.annotation.RetentionPolicy;
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.Iterator;
33import java.util.List;
34import java.util.Set;
35import java.util.WeakHashMap;
36
37/**
38 * Base class for all IMS features that are supported by the framework.
39 * @hide
40 */
41public abstract class ImsFeature {
42
43    private static final String LOG_TAG = "ImsFeature";
44
45    /**
46     * Action to broadcast when ImsService is up.
47     * Internal use only.
48     * Only defined here separately compatibility purposes with the old ImsService.
49     * @hide
50     */
51    public static final String ACTION_IMS_SERVICE_UP =
52            "com.android.ims.IMS_SERVICE_UP";
53
54    /**
55     * Action to broadcast when ImsService is down.
56     * Internal use only.
57     * Only defined here separately for compatibility purposes with the old ImsService.
58     * @hide
59     */
60    public static final String ACTION_IMS_SERVICE_DOWN =
61            "com.android.ims.IMS_SERVICE_DOWN";
62
63    /**
64     * Part of the ACTION_IMS_SERVICE_UP or _DOWN intents.
65     * A long value; the phone ID corresponding to the IMS service coming up or down.
66     * Only defined here separately for compatibility purposes with the old ImsService.
67     * @hide
68     */
69    public static final String EXTRA_PHONE_ID = "android:phone_id";
70
71    // Invalid feature value
72    public static final int INVALID = -1;
73    // ImsFeatures that are defined in the Manifests. Ensure that these values match the previously
74    // defined values in ImsServiceClass for compatibility purposes.
75    public static final int EMERGENCY_MMTEL = 0;
76    public static final int MMTEL = 1;
77    public static final int RCS = 2;
78    // Total number of features defined
79    public static final int MAX = 3;
80
81    // Integer values defining the state of the ImsFeature at any time.
82    @IntDef(flag = true,
83            value = {
84                    STATE_NOT_AVAILABLE,
85                    STATE_INITIALIZING,
86                    STATE_READY,
87            })
88    @Retention(RetentionPolicy.SOURCE)
89    public @interface ImsState {}
90    public static final int STATE_NOT_AVAILABLE = 0;
91    public static final int STATE_INITIALIZING = 1;
92    public static final int STATE_READY = 2;
93
94    private List<INotifyFeatureRemoved> mRemovedListeners = new ArrayList<>();
95    private final Set<IImsFeatureStatusCallback> mStatusCallbacks = Collections.newSetFromMap(
96            new WeakHashMap<IImsFeatureStatusCallback, Boolean>());
97    private @ImsState int mState = STATE_NOT_AVAILABLE;
98    private int mSlotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
99    private Context mContext;
100
101    public interface INotifyFeatureRemoved {
102        void onFeatureRemoved(int slotId);
103    }
104
105    public void setContext(Context context) {
106        mContext = context;
107    }
108
109    public void setSlotId(int slotId) {
110        mSlotId = slotId;
111    }
112
113    public void addFeatureRemovedListener(INotifyFeatureRemoved listener) {
114        synchronized (mRemovedListeners) {
115            mRemovedListeners.add(listener);
116        }
117    }
118
119    public void removeFeatureRemovedListener(INotifyFeatureRemoved listener) {
120        synchronized (mRemovedListeners) {
121            mRemovedListeners.remove(listener);
122        }
123    }
124
125    // Not final for testing.
126    public void notifyFeatureRemoved(int slotId) {
127        synchronized (mRemovedListeners) {
128            mRemovedListeners.forEach(l -> l.onFeatureRemoved(slotId));
129            onFeatureRemoved();
130        }
131    }
132
133    public int getFeatureState() {
134        return mState;
135    }
136
137    protected final void setFeatureState(@ImsState int state) {
138        if (mState != state) {
139            mState = state;
140            notifyFeatureState(state);
141        }
142    }
143
144    public void addImsFeatureStatusCallback(IImsFeatureStatusCallback c) {
145        if (c == null) {
146            return;
147        }
148        try {
149            // If we have just connected, send queued status.
150            c.notifyImsFeatureStatus(mState);
151            // Add the callback if the callback completes successfully without a RemoteException.
152            synchronized (mStatusCallbacks) {
153                mStatusCallbacks.add(c);
154            }
155        } catch (RemoteException e) {
156            Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage());
157        }
158    }
159
160    public void removeImsFeatureStatusCallback(IImsFeatureStatusCallback c) {
161        if (c == null) {
162            return;
163        }
164        synchronized (mStatusCallbacks) {
165            mStatusCallbacks.remove(c);
166        }
167    }
168
169    /**
170     * Internal method called by ImsFeature when setFeatureState has changed.
171     * @param state
172     */
173    private void notifyFeatureState(@ImsState int state) {
174        synchronized (mStatusCallbacks) {
175            for (Iterator<IImsFeatureStatusCallback> iter = mStatusCallbacks.iterator();
176                 iter.hasNext(); ) {
177                IImsFeatureStatusCallback callback = iter.next();
178                try {
179                    Log.i(LOG_TAG, "notifying ImsFeatureState=" + state);
180                    callback.notifyImsFeatureStatus(state);
181                } catch (RemoteException e) {
182                    // remove if the callback is no longer alive.
183                    iter.remove();
184                    Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage());
185                }
186            }
187        }
188        sendImsServiceIntent(state);
189    }
190
191    /**
192     * Provide backwards compatibility using deprecated service UP/DOWN intents.
193     */
194    private void sendImsServiceIntent(@ImsState int state) {
195        if(mContext == null || mSlotId == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
196            return;
197        }
198        Intent intent;
199        switch (state) {
200            case ImsFeature.STATE_NOT_AVAILABLE:
201            case ImsFeature.STATE_INITIALIZING:
202                intent = new Intent(ACTION_IMS_SERVICE_DOWN);
203                break;
204            case ImsFeature.STATE_READY:
205                intent = new Intent(ACTION_IMS_SERVICE_UP);
206                break;
207            default:
208                intent = new Intent(ACTION_IMS_SERVICE_DOWN);
209        }
210        intent.putExtra(EXTRA_PHONE_ID, mSlotId);
211        mContext.sendBroadcast(intent);
212    }
213
214    /**
215     * Called when the feature is being removed and must be cleaned up.
216     */
217    public abstract void onFeatureRemoved();
218}
219