HoldingConnectionClient.java
/*
* Copyright 2019 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 androidx.ads.identifier.internal;
import static androidx.ads.identifier.AdvertisingIdUtils.GET_AD_ID_ACTION;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import androidx.ads.identifier.AdvertisingIdNotAvailableException;
import androidx.ads.identifier.AdvertisingIdUtils;
import androidx.ads.identifier.provider.IAdvertisingIdService;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
/** A client which keeps the ServiceConnection to the {@link IAdvertisingIdService}. */
public class HoldingConnectionClient {
private static final long SERVICE_CONNECTION_TIMEOUT_SECONDS = 10;
private final Context mContext;
@NonNull
private final BlockingServiceConnection mConnection;
@NonNull
private final String mPackageName;
@NonNull
private final IAdvertisingIdService mIdService;
/**
* The last connection ID which assign to the users of this client.
*
* <p>This also indicates the connection status, >= 0 indicates this client is connected,
* otherwise this client has already been disconnected.
* <p>It helps to synchronize between the usages of this client and auto disconnection task by
* using this single atomic, which supports 3 kinds of atomic operations:
* <ul>
* <li>Checks whether this client is connected, if yes, increment and get a connection ID.
* <li>When an auto disconnect task is due, it compares its connection ID to this value, if
* same, unbind the service and sets this atomic to {@link Long#MIN_VALUE}.
* <li>When this client's connection has lost and
* {@link BlockingServiceConnection#onServiceDisconnected} is called, unbind the service
* and sets this atomic to {@link Long#MIN_VALUE}.
* </ul>
* <p>This ID is monotonically increasing, except when this client is disconnected, this ID
* sets to {@link Long#MIN_VALUE}.
*/
private final AtomicLong mLastConnectionId = new AtomicLong(0);
@WorkerThread
public HoldingConnectionClient(@NonNull Context context)
throws AdvertisingIdNotAvailableException, IOException, TimeoutException,
InterruptedException {
mContext = context;
ComponentName componentName = getProviderComponentName(mContext);
mConnection = getServiceConnection(componentName);
mIdService = getIdServiceFromConnection();
mPackageName = componentName.getPackageName();
}
/** Gets the connected {@link IAdvertisingIdService}. */
@NonNull
public IAdvertisingIdService getIdService() {
return mIdService;
}
/** Gets the connected service's package name. */
@NonNull
public String getPackageName() {
return mPackageName;
}
/** Gets whether the client is connected to the {@link IAdvertisingIdService}. */
public boolean isConnected() {
return mLastConnectionId.get() >= 0;
}
/**
* Gets a connection ID before using this client which prevents race condition with the auto
* disconnection task.
*
* @return connection ID, >= 0 indicates this client is connected, otherwise this client has
* already been disconnected.
*/
public long askConnectionId() {
return mLastConnectionId.incrementAndGet();
}
/**
* Closes the connection to the Advertising ID Provider Service.
*
* <p>Note: If the connection has already been closed, does nothing.
*/
void finish() {
if (mLastConnectionId.getAndSet(Long.MIN_VALUE) >= 0) {
mContext.unbindService(mConnection);
}
}
/**
* Tries to close the connection to the Advertising ID Provider Service if no one is using the
* client.
*
* @return true if this client is disconnected after this method returns.
*/
public boolean tryFinish(long connectionId) {
if (mLastConnectionId.compareAndSet(connectionId, Long.MIN_VALUE)) {
mContext.unbindService(mConnection);
return true;
}
return !isConnected();
}
private static ComponentName getProviderComponentName(Context context)
throws AdvertisingIdNotAvailableException {
PackageManager packageManager = context.getPackageManager();
List<ServiceInfo> serviceInfos =
AdvertisingIdUtils.getAdvertisingIdProviderServices(packageManager);
ServiceInfo serviceInfo =
AdvertisingIdUtils.selectServiceByPriority(serviceInfos, packageManager);
if (serviceInfo == null) {
throw new AdvertisingIdNotAvailableException(
"No compatible AndroidX Advertising ID Provider available.");
}
return new ComponentName(serviceInfo.packageName, serviceInfo.name);
}
/**
* Retrieves BlockingServiceConnection which must be unbound after use.
*
* @throws IOException when unable to bind service successfully.
*/
@VisibleForTesting
BlockingServiceConnection getServiceConnection(ComponentName componentName) throws IOException {
Intent intent = new Intent(GET_AD_ID_ACTION);
intent.setComponent(componentName);
BlockingServiceConnection bsc = new BlockingServiceConnection();
if (mContext.bindService(intent, bsc, Service.BIND_AUTO_CREATE)) {
return bsc;
} else {
throw new IOException("Connection failure");
}
}
/**
* Gets the {@link IAdvertisingIdService} from the blocking queue. This should wait until
* {@link ServiceConnection#onServiceConnected} event with a
* {@link #SERVICE_CONNECTION_TIMEOUT_SECONDS} second timeout.
*
* @throws TimeoutException if connection timeout period has expired.
* @throws InterruptedException if connection has been interrupted before connected.
*/
@VisibleForTesting
@WorkerThread
IAdvertisingIdService getIdServiceFromConnection()
throws TimeoutException, InterruptedException {
// Block until the bind is complete, or timeout period is over.
return IAdvertisingIdService.Stub.asInterface(mConnection.getServiceWithTimeout());
}
/**
* A one-time use ServiceConnection that facilitates waiting for the bind to complete and the
* passing of the IBinder from the callback thread to the waiting thread.
*/
class BlockingServiceConnection implements ServiceConnection {
// Facilitates passing of the IBinder across threads
private final BlockingQueue<IBinder> mBlockingQueue = new LinkedBlockingQueue<>();
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBlockingQueue.add(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
finish();
}
/**
* Blocks until the bind is complete with a timeout and returns the bound IBinder. This must
* only be called once.
*
* @return the IBinder of the bound service
* @throws InterruptedException if the current thread is interrupted while waiting for
* the bind
* @throws TimeoutException if the timeout period has elapsed
*/
@WorkerThread
@NonNull
IBinder getServiceWithTimeout() throws InterruptedException, TimeoutException {
IBinder binder =
mBlockingQueue.poll(SERVICE_CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (binder == null) {
throw new TimeoutException("Timed out waiting for the service connection");
}
return binder;
}
}
}