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.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.RemoteException;
import androidx.ads.identifier.internal.BlockingServiceConnection;
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 androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 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 {
private static final long SERVICE_CONNECTION_TIMEOUT_SECONDS = 10;
@VisibleForTesting
static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
@Nullable
private BlockingServiceConnection mConnection;
@Nullable
private IAdvertisingIdService mService;
private final Context mContext;
private ComponentName mComponentName;
/** Constructs a new {@link AdvertisingIdClient} object. */
@VisibleForTesting
AdvertisingIdClient(Context context) {
Preconditions.checkNotNull(context);
mContext = context.getApplicationContext();
}
@WorkerThread
private void start() throws IOException, AdvertisingIdNotAvailableException, TimeoutException,
InterruptedException {
if (mConnection == null) {
mComponentName = getProviderComponentName(mContext);
mConnection = getServiceConnection();
mService = getAdvertisingIdService(mConnection);
}
}
/** Returns the Advertising ID info as {@link AdvertisingIdInfo}. */
@VisibleForTesting
@WorkerThread
AdvertisingIdInfo getInfoInternal() throws IOException, AdvertisingIdNotAvailableException,
TimeoutException, InterruptedException {
if (mConnection == null) {
start();
}
try {
String id = mService.getId();
if (id == null || id.trim().isEmpty()) {
throw new AdvertisingIdNotAvailableException(
"Advertising ID Provider does not returns an Advertising ID.");
}
return AdvertisingIdInfo.builder()
.setId(normalizeId(id))
.setProviderPackageName(mComponentName.getPackageName())
.setLimitAdTrackingEnabled(mService.isLimitAdTrackingEnabled())
.build();
} catch (RemoteException e) {
throw new IOException("Remote exception", e);
} catch (RuntimeException e) {
throw new AdvertisingIdNotAvailableException(
"Advertising ID Provider throws a exception.", e);
}
}
/**
* Checks the Advertising ID format, if it's not in UUID format, normalizes the Advertising
* ID to UUID format.
*
* @return Advertising ID, in lower case format using locale {@code Locale.US};
*/
@VisibleForTesting
static String normalizeId(String id) {
String lowerCaseId = id.toLowerCase(Locale.US);
if (isUuidFormat(lowerCaseId)) {
return lowerCaseId;
}
return UUID.nameUUIDFromBytes(id.getBytes(Charset.forName("UTF-8"))).toString();
}
/* Validate the input is lowercase and is a valid UUID. */
private static boolean isUuidFormat(String id) {
try {
return id.equals(UUID.fromString(id).toString());
} catch (IllegalArgumentException iae) {
return false;
}
}
/** Closes the connection to the Advertising ID Provider Service. */
@VisibleForTesting
void finish() {
if (mConnection == null) {
return;
}
mContext.unbindService(mConnection);
mComponentName = null;
mConnection = null;
mService = null;
}
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() throws IOException {
Intent intent = new Intent(AdvertisingIdUtils.GET_AD_ID_ACTION);
intent.setComponent(mComponentName);
BlockingServiceConnection bsc = new BlockingServiceConnection();
if (mContext.bindService(intent, bsc, Service.BIND_AUTO_CREATE)) {
return bsc;
} else {
throw new IOException("Connection failure");
}
}
/**
* Get the {@link IAdvertisingIdService} from the blocking queue. This should wait until
* {@link android.content.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 getAdvertisingIdService(BlockingServiceConnection bsc)
throws TimeoutException, InterruptedException {
// Block until the bind is complete, or timeout period is over.
return IAdvertisingIdService.Stub.asInterface(
bsc.getServiceWithTimeout(
SERVICE_CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS));
}
/**
* 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 connection timeout period has expired.
* <li><b>InterruptedException</b> indicating the current thread has been interrupted.
* </ul>
*/
@NonNull
public static ListenableFuture<AdvertisingIdInfo> getAdvertisingIdInfo(
@NonNull Context context) {
return CallbackToFutureAdapter.getFuture(completer -> {
EXECUTOR_SERVICE.execute(() -> {
AdvertisingIdClient client = new AdvertisingIdClient(context);
try {
completer.set(client.getInfoInternal());
} catch (IOException | AdvertisingIdNotAvailableException | TimeoutException
| InterruptedException e) {
completer.setException(e);
} finally {
client.finish();
}
});
return "getAdvertisingIdInfo";
});
}
}