public class

AdvertisingIdClient

extends java.lang.Object

 java.lang.Object

↳androidx.ads.identifier.AdvertisingIdClient

Gradle dependencies

compile group: 'androidx.ads', name: 'ads-identifier', version: '1.0.0-alpha04'

  • groupId: androidx.ads
  • artifactId: ads-identifier
  • version: 1.0.0-alpha04

Artifact androidx.ads:ads-identifier:1.0.0-alpha04 it located at Google repository (https://maven.google.com/)

Overview

Client for retrieving Advertising ID related info from an AndroidX ID Provider installed on the device.

Typical usage would be:

  1. Call AdvertisingIdClient.isAdvertisingIdProviderAvailable(Context) to make sure there is an Advertising ID Provider available.
  2. Call AdvertisingIdClient.getAdvertisingIdInfo(Context) to get Advertising ID info (the Advertising ID and LAT setting).

Summary

Methods
public static <any>getAdvertisingIdInfo(Context context)

Retrieves the user's Advertising ID info.

public static booleanisAdvertisingIdProviderAvailable(Context context)

Checks whether there is any Advertising ID Provider installed on the device.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static boolean isAdvertisingIdProviderAvailable(Context context)

Checks whether there is any Advertising ID Provider installed on the device.

This method does a quick check for the Advertising ID providers.

Note: Even if this method returns true, there is still a possibility that the AdvertisingIdClient.getAdvertisingIdInfo(Context) method throws an exception for some reason.

Parameters:

context: Current (such as the current ).

Returns:

whether there is an Advertising ID Provider available on the device.

public static <any> getAdvertisingIdInfo(Context context)

Retrieves the user's Advertising ID info.

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:

  1. System-level providers with "androidx.ads.identifier.provider.HIGH_PRIORITY" permission
  2. Other system-level providers

If there are ties in any of the above categories, it will use this priority:

  1. First app by earliest install time ()
  2. First app by package name alphabetically sorted

Parameters:

context: Current (such as the current ).

Returns:

A that will be fulfilled with a AdvertisingIdInfo which contains the user's Advertising ID info, or rejected with the following exceptions,

  • IOException signaling connection to Advertising ID Providers failed.
  • AdvertisingIdNotAvailableException indicating Advertising ID is not available, like no Advertising ID Provider found or provider does not return an Advertising ID.
  • TimeoutException indicating timeout period (20s) has expired.
  • InterruptedException indicating the current thread has been interrupted.

Source

/*
 * 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);
    }
}