| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.telephony; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.telephony.IBootstrapAuthenticationCallback; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.telephony.gba.GbaAuthRequest; |
| import android.telephony.gba.GbaService; |
| import android.telephony.gba.IGbaService; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.telephony.Rlog; |
| |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| |
| /** |
| * Class that serves as the layer between GbaService and ServiceStateTracker. It helps binding, |
| * sending request, receiving callback, and registering for state change to GbaService. |
| */ |
| public class GbaManager { |
| private static final boolean DBG = Build.IS_DEBUGGABLE; |
| private static final int EVENT_BIND_SERVICE = 1; |
| private static final int EVENT_UNBIND_SERVICE = 2; |
| private static final int EVENT_BIND_FAIL = 3; |
| private static final int EVENT_BIND_SUCCESS = 4; |
| private static final int EVENT_CONFIG_CHANGED = 5; |
| private static final int EVENT_REQUESTS_RECEIVED = 6; |
| |
| @VisibleForTesting |
| public static final int RETRY_TIME_MS = 3000; |
| @VisibleForTesting |
| public static final int MAX_RETRY = 5; |
| @VisibleForTesting |
| public static final int REQUEST_TIMEOUT_MS = 5000; |
| |
| private final String mLogTag; |
| private final Context mContext; |
| private final int mSubId; |
| |
| private IGbaService mIGbaService; |
| private GbaDeathRecipient mDeathRecipient; |
| private String mTargetBindingPackageName; |
| private GbaServiceConnection mServiceConnection; |
| private Handler mHandler; |
| |
| private String mServicePackageName; |
| private String mServicePackageNameOverride; |
| private int mReleaseTime; |
| private int mRetryTimes = 0; |
| |
| //the requests to be sent to the GBA service |
| private final ConcurrentLinkedQueue<GbaAuthRequest> mRequestQueue = |
| new ConcurrentLinkedQueue<>(); |
| //the callbacks of the pending requests which have been sent to the GBA service |
| private final SparseArray<IBootstrapAuthenticationCallback> mCallbacks = new SparseArray<>(); |
| |
| private static final SparseArray<GbaManager> sGbaManagers = new SparseArray<>(); |
| |
| private final class GbaManagerHandler extends Handler { |
| GbaManagerHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| logv("handle msg:" + msg.what); |
| switch (msg.what) { |
| case EVENT_BIND_SERVICE: |
| if (mRetryTimes++ < MAX_RETRY) { |
| rebindService(false); |
| } else { |
| loge("Too many retries, stop now!"); |
| sendEmptyMessage(EVENT_BIND_FAIL); |
| } |
| break; |
| case EVENT_UNBIND_SERVICE: |
| //do nothing if new requests are coming |
| if (mRequestQueue.isEmpty()) { |
| clearCallbacksAndNotifyFailure(); |
| unbindService(); |
| } |
| break; |
| case EVENT_BIND_FAIL: |
| case EVENT_BIND_SUCCESS: |
| mRetryTimes = 0; |
| processRequests(); |
| break; |
| case EVENT_REQUESTS_RECEIVED: |
| if (isServiceConnected()) { |
| processRequests(); |
| } else { |
| if (!mHandler.hasMessages(EVENT_BIND_SERVICE)) { |
| mHandler.sendEmptyMessage(EVENT_BIND_SERVICE); |
| } |
| } |
| break; |
| case EVENT_CONFIG_CHANGED: |
| mRetryTimes = 0; |
| if (isServiceConnetable() || isServiceConnected()) { |
| //force to rebind when config is changed |
| rebindService(true); |
| } |
| break; |
| default: |
| loge("Unhandled event " + msg.what); |
| } |
| } |
| } |
| |
| private final class GbaDeathRecipient implements IBinder.DeathRecipient { |
| |
| private final ComponentName mComponentName; |
| private IBinder mBinder; |
| |
| GbaDeathRecipient(ComponentName name) { |
| mComponentName = name; |
| } |
| |
| public void linkToDeath(IBinder service) throws RemoteException { |
| if (service != null) { |
| mBinder = service; |
| mBinder.linkToDeath(this, 0); |
| } |
| } |
| |
| public synchronized void unlinkToDeath() { |
| if (mBinder != null) { |
| mBinder.unlinkToDeath(this, 0); |
| mBinder = null; |
| } |
| } |
| |
| @Override |
| public void binderDied() { |
| logd("GbaService(" + mComponentName + ") has died."); |
| unlinkToDeath(); |
| //retry if died |
| retryBind(); |
| } |
| } |
| |
| private final class GbaServiceConnection implements ServiceConnection { |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| logd("service " + name + " for Gba is connected."); |
| mIGbaService = IGbaService.Stub.asInterface(service); |
| mDeathRecipient = new GbaDeathRecipient(name); |
| try { |
| mDeathRecipient.linkToDeath(service); |
| } catch (RemoteException exception) { |
| // Remote exception means that the binder already died. |
| mDeathRecipient.binderDied(); |
| logd("RemoteException " + exception); |
| } |
| mHandler.sendEmptyMessage(EVENT_BIND_SUCCESS); |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName name) { |
| logd("service " + name + " is now disconnected."); |
| mTargetBindingPackageName = null; |
| } |
| } |
| |
| @VisibleForTesting |
| public GbaManager(Context context, int subId, String servicePackageName, int releaseTime) { |
| mContext = context; |
| mSubId = subId; |
| mLogTag = "GbaManager[" + subId + "]"; |
| |
| mServicePackageName = servicePackageName; |
| mReleaseTime = releaseTime; |
| |
| HandlerThread headlerThread = new HandlerThread(mLogTag); |
| headlerThread.start(); |
| mHandler = new GbaManagerHandler(headlerThread.getLooper()); |
| |
| if (mReleaseTime < 0) { |
| mHandler.sendEmptyMessage(EVENT_BIND_SERVICE); |
| } |
| } |
| |
| /** |
| * create a GbaManager instance for a sub |
| */ |
| public static GbaManager make(Context context, int subId, |
| String servicePackageName, int releaseTime) { |
| GbaManager gm = new GbaManager(context, subId, servicePackageName, releaseTime); |
| synchronized (sGbaManagers) { |
| sGbaManagers.put(subId, gm); |
| } |
| return gm; |
| } |
| |
| /** |
| * get singleton instance of GbaManager |
| * @return GbaManager |
| */ |
| public static GbaManager getInstance(int subId) { |
| synchronized (sGbaManagers) { |
| return sGbaManagers.get(subId); |
| } |
| } |
| |
| /** |
| * handle the bootstrap authentication request |
| * @hide |
| */ |
| public void bootstrapAuthenticationRequest(GbaAuthRequest req) { |
| logv("bootstrapAuthenticationRequest: " + req); |
| //No GBA service configured |
| if (TextUtils.isEmpty(getServicePackage())) { |
| logd("do not support!"); |
| try { |
| req.getCallback().onAuthenticationFailure(req.getToken(), |
| TelephonyManager.GBA_FAILURE_REASON_FEATURE_NOT_SUPPORTED); |
| } catch (RemoteException exception) { |
| loge("exception to call service: " + exception); |
| } |
| return; |
| } |
| |
| mRequestQueue.offer(req); |
| if (!mHandler.hasMessages(EVENT_REQUESTS_RECEIVED)) { |
| mHandler.sendEmptyMessage(EVENT_REQUESTS_RECEIVED); |
| } |
| } |
| |
| private final IBootstrapAuthenticationCallback mServiceCallback = |
| new IBootstrapAuthenticationCallback.Stub() { |
| @Override |
| public void onKeysAvailable(int token, byte[] gbaKey, String btId) { |
| logv("onKeysAvailable: " + Integer.toHexString(token) + ", id: " + btId); |
| |
| IBootstrapAuthenticationCallback cb = null; |
| synchronized (mCallbacks) { |
| cb = mCallbacks.get(token); |
| } |
| if (cb != null) { |
| try { |
| cb.onKeysAvailable(token, gbaKey, btId); |
| } catch (RemoteException exception) { |
| logd("RemoteException " + exception); |
| } |
| synchronized (mCallbacks) { |
| mCallbacks.remove(token); |
| if (mCallbacks.size() == 0) { |
| releaseServiceAsNeeded(0); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onAuthenticationFailure(int token, int reason) { |
| logd("onAuthenticationFailure: " |
| + Integer.toHexString(token) + " for: " + reason); |
| |
| IBootstrapAuthenticationCallback cb = null; |
| synchronized (mCallbacks) { |
| cb = mCallbacks.get(token); |
| } |
| if (cb != null) { |
| try { |
| cb.onAuthenticationFailure(token, reason); |
| } catch (RemoteException exception) { |
| logd("RemoteException " + exception); |
| } |
| synchronized (mCallbacks) { |
| mCallbacks.remove(token); |
| if (mCallbacks.size() == 0) { |
| releaseServiceAsNeeded(0); |
| } |
| } |
| } |
| } |
| }; |
| |
| private void processRequests() { |
| if (isServiceConnected()) { |
| try { |
| while (!mRequestQueue.isEmpty()) { |
| GbaAuthRequest request = new GbaAuthRequest(mRequestQueue.peek()); |
| synchronized (mCallbacks) { |
| mCallbacks.put(request.getToken(), request.getCallback()); |
| } |
| request.setCallback(mServiceCallback); |
| mIGbaService.authenticationRequest(request); |
| mRequestQueue.poll(); |
| } |
| } catch (RemoteException exception) { |
| // Remote exception means that the binder already died. |
| mDeathRecipient.binderDied(); |
| logd("RemoteException " + exception); |
| } |
| } else { |
| while (!mRequestQueue.isEmpty()) { |
| GbaAuthRequest req = mRequestQueue.poll(); |
| try { |
| req.getCallback().onAuthenticationFailure(req.getToken(), |
| TelephonyManager.GBA_FAILURE_REASON_FEATURE_NOT_SUPPORTED); |
| } catch (RemoteException exception) { |
| logd("RemoteException " + exception); |
| } |
| } |
| } |
| |
| releaseServiceAsNeeded(REQUEST_TIMEOUT_MS); |
| } |
| |
| private void releaseServiceAsNeeded(int timeout) { |
| int configReleaseTime = getReleaseTime(); |
| //always on |
| if (configReleaseTime < 0) { |
| return; |
| } |
| //schedule to release service |
| int delayTime = configReleaseTime > timeout ? configReleaseTime : timeout; |
| if (mHandler.hasMessages(EVENT_UNBIND_SERVICE)) { |
| mHandler.removeMessages(EVENT_UNBIND_SERVICE); |
| } |
| mHandler.sendEmptyMessageDelayed(EVENT_UNBIND_SERVICE, delayTime); |
| } |
| |
| private void clearCallbacksAndNotifyFailure() { |
| synchronized (mCallbacks) { |
| for (int i = 0; i < mCallbacks.size(); i++) { |
| IBootstrapAuthenticationCallback cb = mCallbacks.valueAt(i); |
| try { |
| cb.onAuthenticationFailure(mCallbacks.keyAt(i), |
| TelephonyManager.GBA_FAILURE_REASON_UNKNOWN); |
| } catch (RemoteException exception) { |
| logd("RemoteException " + exception); |
| } |
| } |
| mCallbacks.clear(); |
| } |
| } |
| |
| /** return if GBA service has been connected */ |
| @VisibleForTesting |
| public boolean isServiceConnected() { |
| //current bound service should be the updated service package. |
| synchronized (this) { |
| return (mIGbaService != null) && (mIGbaService.asBinder().isBinderAlive()) |
| && TextUtils.equals(mServicePackageName, mTargetBindingPackageName); |
| } |
| } |
| |
| private boolean isServiceConnetable() { |
| synchronized (this) { |
| return mTargetBindingPackageName != null || ( |
| mReleaseTime < 0 && !TextUtils.isEmpty(mServicePackageName)); |
| } |
| } |
| |
| private void unbindService() { |
| if (mDeathRecipient != null) { |
| mDeathRecipient.unlinkToDeath(); |
| } |
| if (mServiceConnection != null) { |
| logv("unbind service."); |
| mContext.unbindService(mServiceConnection); |
| } |
| mDeathRecipient = null; |
| mIGbaService = null; |
| mServiceConnection = null; |
| mTargetBindingPackageName = null; |
| } |
| |
| private void bindService() { |
| if (mContext == null || !SubscriptionManager.isValidSubscriptionId(mSubId)) { |
| loge("Can't bind service with invalid sub Id."); |
| return; |
| } |
| |
| String servicePackage = getServicePackage(); |
| if (TextUtils.isEmpty(servicePackage)) { |
| loge("Can't find the binding package"); |
| return; |
| } |
| |
| Intent intent = new Intent(GbaService.SERVICE_INTERFACE); |
| intent.setPackage(servicePackage); |
| |
| try { |
| logv("Trying to bind " + servicePackage); |
| mServiceConnection = new GbaServiceConnection(); |
| if (!mContext.bindService(intent, mServiceConnection, |
| Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE)) { |
| logd("Cannot bind to the service."); |
| retryBind(); |
| return; |
| } |
| mTargetBindingPackageName = servicePackage; |
| } catch (SecurityException exception) { |
| loge("bindService failed " + exception); |
| } |
| } |
| |
| private void retryBind() { |
| //do nothing if binding service has been scheduled |
| if (mHandler.hasMessages(EVENT_BIND_SERVICE)) { |
| logv("wait for pending retry."); |
| return; |
| } |
| |
| logv("starting retry:" + mRetryTimes); |
| |
| mHandler.sendEmptyMessageDelayed(EVENT_BIND_SERVICE, RETRY_TIME_MS); |
| } |
| |
| private void rebindService(boolean isForce) { |
| // Do nothing if no need to rebind. |
| if (!isForce && isServiceConnected()) { |
| logv("Service " + getServicePackage() + " already bound or being bound."); |
| return; |
| } |
| |
| unbindService(); |
| bindService(); |
| } |
| |
| /** override GBA service package name to be connected */ |
| public boolean overrideServicePackage(String packageName) { |
| synchronized (this) { |
| if (!TextUtils.equals(mServicePackageName, packageName)) { |
| logv("Service package name is changed from " + mServicePackageName |
| + " to " + packageName); |
| mServicePackageName = packageName; |
| if (!mHandler.hasMessages(EVENT_CONFIG_CHANGED)) { |
| mHandler.sendEmptyMessage(EVENT_CONFIG_CHANGED); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** return GBA service package name */ |
| public String getServicePackage() { |
| synchronized (this) { |
| return mServicePackageName; |
| } |
| } |
| |
| /** override the release time to unbind GBA service after the request is handled */ |
| public boolean overrideReleaseTime(int interval) { |
| synchronized (this) { |
| if (mReleaseTime != interval) { |
| logv("Service release time is changed from " + mReleaseTime |
| + " to " + interval); |
| mReleaseTime = interval; |
| if (!mHandler.hasMessages(EVENT_CONFIG_CHANGED)) { |
| mHandler.sendEmptyMessage(EVENT_CONFIG_CHANGED); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** return the release time to unbind GBA service after the request is handled */ |
| public int getReleaseTime() { |
| synchronized (this) { |
| return mReleaseTime; |
| } |
| } |
| |
| @VisibleForTesting |
| public Handler getHandler() { |
| return mHandler; |
| } |
| |
| /** only for testing */ |
| @VisibleForTesting |
| public void destroy() { |
| mHandler.removeCallbacksAndMessages(null); |
| mHandler.getLooper().quit(); |
| mRequestQueue.clear(); |
| mCallbacks.clear(); |
| unbindService(); |
| sGbaManagers.remove(mSubId); |
| } |
| |
| private void logv(String msg) { |
| if (DBG) { |
| Rlog.d(mLogTag, msg); |
| } |
| } |
| |
| private void logd(String msg) { |
| Rlog.d(mLogTag, msg); |
| } |
| |
| private void loge(String msg) { |
| Rlog.e(mLogTag, msg); |
| } |
| } |