ProviderInfoRetriever.java

/*
 * 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 androidx.wear.complications;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.wearable.complications.ComplicationProviderInfo;
import android.support.wearable.complications.IProviderInfoService;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.concurrent.ExecutionException;

/**
 * Retrieves {@link ComplicationProviderInfo} for a watch face's complications.
 *
 * <p>To use construct an instance and call {@link #retrieveProviderInfo} which returns a {@link
 * ListenableFuture}.
 *
 * <p>Further calls to {@link #retrieveProviderInfo} may be made using the same instance of this
 * class, but {@link #close} must be called when it is no longer needed. Once release has been
 * called, further retrieval attempts will fail.
 */
public class ProviderInfoRetriever implements AutoCloseable {

    /** Results for {@link #retrieveProviderInfo}. */
    public static class ProviderInfo {
        /** The id for the complication, as provided to {@link #retrieveProviderInfo}. */
        private final int mWatchFaceComplicationId;

        /**
         * Details of the provider for that complication, or {@code null} if no provider is
         * currently configured.
         */
        @Nullable
        private final ComplicationProviderInfo mInfo;

        ProviderInfo(int watchFaceComplicationId, @Nullable ComplicationProviderInfo info) {
            mWatchFaceComplicationId = watchFaceComplicationId;
            mInfo = info;
        }

        /**
         * Returns the id for the complication, as provided to {@link #retrieveProviderInfo}.
         */
        public int getWatchFaceComplicationId() {
            return mWatchFaceComplicationId;
        }

        /**
         * Return details of the provider for that complication, or {@code null} if no provider is
         * currently configured.
         */
        @Nullable
        public ComplicationProviderInfo getInfo() {
            return mInfo;
        }
    }

    private final class ProviderInfoServiceConnection implements ServiceConnection {
        @Override
        @SuppressLint("SyntheticAccessor")
        public void onServiceConnected(ComponentName name, IBinder service) {
            mServiceFuture.set(IProviderInfoService.Stub.asInterface(service));
        }

        @Override
        @SuppressLint("SyntheticAccessor")
        public void onServiceDisconnected(ComponentName name) {
            mServiceFuture.cancel(false);
        }
    }

    /** The package of the service that supplies provider info. */
    private static final String PROVIDER_INFO_SERVICE_PACKAGE = "com.google.android.wearable.app";

    private static final String ACTION_GET_COMPLICATION_CONFIG =
            "android.support.wearable.complications.ACTION_GET_COMPLICATION_CONFIG";

    @SuppressLint("SyntheticAccessor")
    private final ServiceConnection mConn = new ProviderInfoServiceConnection();

    @NonNull private final Context mContext;

    private final ResolvableFuture<IProviderInfoService> mServiceFuture = ResolvableFuture.create();

    /**
     * @param context the current context
     */
    public ProviderInfoRetriever(@NonNull Context context) {
        mContext = context;

        Intent intent = new Intent(ACTION_GET_COMPLICATION_CONFIG);
        intent.setPackage(PROVIDER_INFO_SERVICE_PACKAGE);
        mContext.bindService(intent, mConn, Context.BIND_AUTO_CREATE);
    }

    /**
     * Requests {@link ComplicationProviderInfo} for the specified complication ids on the specified
     * watch face. When the info is received, the listener will receive a callback for each id.
     * These callbacks will occur on the main thread.
     *
     * <p>This will only work if the package of the current app is the same as the package of the
     * specified watch face.
     *
     * @param watchFaceComponent the ComponentName of the WatchFaceService for which info is
     *     being requested
     * @param watchFaceComplicationIds ids of the complications that info is being requested for
     * @return A {@link ListenableFuture} for the requested provider info. If the look up fails
     *     null will be returned
     */
    @NonNull
    public ListenableFuture<ProviderInfo[]> retrieveProviderInfo(
            @NonNull final ComponentName watchFaceComponent,
            @NonNull final int[] watchFaceComplicationIds) {
        final ResolvableFuture<ProviderInfo[]> mResultFuture = ResolvableFuture.create();
        mServiceFuture.addListener(
                () -> {
                    try {
                        if (mServiceFuture.isCancelled()) {
                            mResultFuture.set(null);
                            return;
                        }
                        ComplicationProviderInfo[] infos =
                                mServiceFuture.get().getProviderInfos(
                                        watchFaceComponent, watchFaceComplicationIds);
                        if (infos != null) {
                            ProviderInfo[] providerInfo = new ProviderInfo[infos.length];
                            for (int i = 0; i < infos.length; i++) {
                                final int watchFaceComplicationId = watchFaceComplicationIds[i];
                                final ComplicationProviderInfo info = infos[i];
                                providerInfo[i] = new ProviderInfo(watchFaceComplicationId, info);
                            }
                            mResultFuture.set(providerInfo);
                        } else {
                            mResultFuture.set(null);
                        }
                    } catch (RemoteException e) {
                        mResultFuture.setException(e);
                    } catch (InterruptedException e) {
                        mResultFuture.setException(e);
                    } catch (ExecutionException e) {
                        mResultFuture.setException(e);
                    }
                },
                runnable -> runnable.run()
        );
        return mResultFuture;
    }

    /**
     * Releases the connection to the complication system used by this class. This must
     * be called when the retriever is no longer needed.
     *
     * <p>Any outstanding or subsequent futures returned by {@link #retrieveProviderInfo} will
     * resolve with null.
     *
     * This class implements the Java {@code AutoClosable} interface and
     * may be used with try-with-resources.
     */
    @Override
    public void close() {
        mContext.unbindService(mConn);
    }
}