AdvertisingIdClient.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;
import android.content.Context;
import android.os.RemoteException;
import androidx.ads.identifier.internal.HoldingConnectionClient;
import androidx.ads.identifier.provider.IAdvertisingIdService;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.auto.value.AutoValue;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Client for retrieving Advertising ID related info from an AndroidX ID Provider installed on
* the device.
*
* <p>Typical usage would be:
* <ol>
* <li>Call {@link #isAdvertisingIdProviderAvailable} to make sure there is an Advertising ID
* Provider available.
* <li>Call {@link #getAdvertisingIdInfo} to get Advertising ID info (the Advertising ID and LAT
* setting).
* </ol>
*/
public class AdvertisingIdClient {
/**
* Amount of time to wait before timing out when trying to get the ID info from the
* Provider. Including the binding service time and the remote calling time.
*/
private static final long TIMEOUT_SECONDS = 20;
private static final long AUTO_DISCONNECT_SECONDS = 30;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static final ExecutorService QUERY_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
Executors.newSingleThreadScheduledExecutor();
private static final Object sLock = new Object();
/**
* The client holding connection which can be reused if connected.
*
* <p>This value will only be set at 2 places at production when setup new connection or auto
* disconnect timeout happen, and 1 place at testing when clear connection.
* <p>There could be multiple connection clients in corner cases, but each of them will be
* auto disconnect eventually.
* <p>Each connection client has a last connection ID field, which ties to the connection
* client and also indicates the status of connection. See {@link HoldingConnectionClient}'s
* mLastConnectionId filed for details.
* <p>Each get ID instance will get a pair of connection client and connection ID (which ties
* to the connection client) first, then use this pair to schedule an auto disconnection at
* {@link #AUTO_DISCONNECT_SECONDS} later.
*/
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@NonNull
static final AtomicReference<HoldingConnectionClient> sConnectionClient =
new AtomicReference<>(null);
@AutoValue
abstract static class ConnectionPair {
@NonNull
abstract HoldingConnectionClient getConnectionClient();
abstract long getConnectionId();
@NonNull
static ConnectionPair of(HoldingConnectionClient connectionClient, long connectionId) {
return new AutoValue_AdvertisingIdClient_ConnectionPair(connectionClient, connectionId);
}
}
private AdvertisingIdClient() {
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@WorkerThread
@NonNull
static ConnectionPair getConnection(Context context)
throws IOException, AdvertisingIdNotAvailableException, TimeoutException,
InterruptedException {
ConnectionPair connectionPair = tryConnect();
if (connectionPair == null) {
synchronized (sLock) {
connectionPair = tryConnect();
if (connectionPair == null) {
HoldingConnectionClient connectionClient = new HoldingConnectionClient(context);
sConnectionClient.set(connectionClient);
connectionPair = ConnectionPair.of(connectionClient, 0);
}
}
}
return connectionPair;
}
@Nullable
private static ConnectionPair tryConnect() {
HoldingConnectionClient connectionClient = sConnectionClient.get();
if (connectionClient != null) {
long connectionId = connectionClient.askConnectionId();
if (connectionId >= 0) {
return ConnectionPair.of(connectionClient, connectionId);
}
}
return null;
}
/** Returns the Advertising ID info as {@link AdvertisingIdInfo}. */
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@VisibleForTesting
@WorkerThread
@NonNull
static AdvertisingIdInfo getIdInfo(HoldingConnectionClient connectionClient)
throws IOException, AdvertisingIdNotAvailableException {
IAdvertisingIdService service = connectionClient.getIdService();
try {
String id = service.getId();
if (id == null || id.trim().isEmpty()) {
throw new AdvertisingIdNotAvailableException(
"Advertising ID Provider does not returns an Advertising ID.");
}
return AdvertisingIdInfo.builder()
.setId(id)
.setProviderPackageName(connectionClient.getPackageName())
.setLimitAdTrackingEnabled(service.isLimitAdTrackingEnabled())
.build();
} catch (RemoteException e) {
throw new IOException("Remote exception", e);
} catch (RuntimeException e) {
throw new AdvertisingIdNotAvailableException(
"Advertising ID Provider throws a exception.", e);
}
}
@VisibleForTesting
static void clearConnectionClient() {
sConnectionClient.set(null);
}
@VisibleForTesting
static boolean isConnected() {
HoldingConnectionClient connectionClient = sConnectionClient.get();
return connectionClient != null && connectionClient.isConnected();
}
/**
* Checks whether there is any Advertising ID Provider installed on the device.
*
* <p>This method does a quick check for the Advertising ID providers.
* <p>Note: Even if this method returns true, there is still a possibility that the
* {@link #getAdvertisingIdInfo(Context)} method throws an exception for some reason.
*
* @param context Current {@link Context} (such as the current {@link android.app.Activity}).
* @return whether there is an Advertising ID Provider available on the device.
*/
public static boolean isAdvertisingIdProviderAvailable(@NonNull Context context) {
return !AdvertisingIdUtils.getAdvertisingIdProviderServices(context.getPackageManager())
.isEmpty();
}
/**
* Retrieves the user's Advertising ID info.
*
* <p>When multiple Advertising ID Providers are installed on the device, this method will
* always return the Advertising ID information from same Advertising ID Provider for all
* apps which use this library, using following priority:
* <ol>
* <li>System-level providers with "androidx.ads.identifier.provider.HIGH_PRIORITY" permission
* <li>Other system-level providers
* </ol>
* <p>If there are ties in any of the above categories, it will use this priority:
* <ol>
* <li>First app by earliest install time
* ({@link android.content.pm.PackageInfo#firstInstallTime})
* <li>First app by package name alphabetically sorted
* </ol>
*
* @param context Current {@link Context} (such as the current {@link android.app.Activity}).
* @return A {@link ListenableFuture} that will be fulfilled with a {@link AdvertisingIdInfo}
* which contains the user's Advertising ID info, or rejected with the following exceptions,
* <ul>
* <li><b>IOException</b> signaling connection to Advertising ID Providers failed.
* <li><b>AdvertisingIdNotAvailableException</b> indicating Advertising ID is not available,
* like no Advertising ID Provider found or provider does not return an Advertising ID.
* <li><b>TimeoutException</b> indicating timeout period (20s) has expired.
* <li><b>InterruptedException</b> indicating the current thread has been interrupted.
* </ul>
*/
@NonNull
public static ListenableFuture<AdvertisingIdInfo> getAdvertisingIdInfo(
@NonNull Context context) {
final Context applicationContext = context.getApplicationContext();
return CallbackToFutureAdapter.getFuture(
new CallbackToFutureAdapter.Resolver<AdvertisingIdInfo>() {
@Override
public Object attachCompleter(
@NonNull CallbackToFutureAdapter.Completer<AdvertisingIdInfo>
completer) {
submitAdvertisingIdInfoTask(applicationContext, completer);
return "getAdvertisingIdInfo";
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static void submitAdvertisingIdInfoTask(
final Context applicationContext,
@NonNull final CallbackToFutureAdapter.Completer<AdvertisingIdInfo> completer) {
final Future<?> getIdInfoFuture = QUERY_EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
try {
ConnectionPair connectionPair = getConnection(applicationContext);
scheduleAutoDisconnect(connectionPair);
completer.set(getIdInfo(connectionPair.getConnectionClient()));
} catch (IOException | AdvertisingIdNotAvailableException | TimeoutException
| InterruptedException e) {
completer.setException(e);
}
}
});
scheduleTimeoutCheck(getIdInfoFuture, completer);
}
@SuppressWarnings("FutureReturnValueIgnored")
private static void scheduleTimeoutCheck(
final Future<?> getIdInfoFuture,
@NonNull final CallbackToFutureAdapter.Completer<AdvertisingIdInfo> completer) {
SCHEDULED_EXECUTOR_SERVICE.schedule(new Runnable() {
@Override
public void run() {
if (!getIdInfoFuture.isDone()) {
completer.setException(new TimeoutException());
getIdInfoFuture.cancel(true);
}
}
}, TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
@SuppressWarnings({"WeakerAccess", "FutureReturnValueIgnored"}) /* synthetic accessor */
static void scheduleAutoDisconnect(final ConnectionPair connectionPair) {
SCHEDULED_EXECUTOR_SERVICE.schedule(new Runnable() {
@Override
public void run() {
HoldingConnectionClient connectionClient = connectionPair.getConnectionClient();
if (connectionClient.tryFinish(connectionPair.getConnectionId())) {
sConnectionClient.compareAndSet(connectionClient, null);
}
}
}, AUTO_DISCONNECT_SECONDS, TimeUnit.SECONDS);
}
}