OngoingActivityData.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.ongoingactivity;

import android.app.Notification;
import android.app.PendingIntent;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.content.LocusIdCompat;

/**
 * This class is used internally by the library to represent the data of an OngoingActivity.
 */
public class OngoingActivityData {
    @Nullable
    private Icon mAnimatedIcon = null;
    @Nullable
    private Icon mStaticIcon = null;
    @Nullable
    private OngoingActivityStatus mStatus = null;
    @Nullable
    private PendingIntent mTouchIntent = null;
    @Nullable
    private LocusIdCompat mLocusId = null;
    private int mOngoingActivityId = DEFAULT_ID;

    OngoingActivityData() {
    }

    @NonNull
    NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
        Bundle bundle = new Bundle();
        if (mAnimatedIcon != null) {
            bundle.putParcelable(KEY_ANIMATED_ICON, mAnimatedIcon);
        }
        if (mStaticIcon != null) {
            bundle.putParcelable(KEY_STATIC_ICON, mStaticIcon);
        }
        if (mStatus != null) {
            mStatus.extend(bundle);
        }
        if (mTouchIntent != null) {
            bundle.putParcelable(KEY_TOUCH_INTENT, mTouchIntent);
        }
        if (mLocusId != null) {
            builder.setLocusId(mLocusId);
        }
        if (mOngoingActivityId != DEFAULT_ID) {
            bundle.putInt(KEY_ID, mOngoingActivityId);
        }
        builder.getExtras().putBundle(EXTRA_ONGOING_ACTIVITY, bundle);
        return builder;
    }

    @NonNull Notification extendAndBuild(@NonNull NotificationCompat.Builder builder) {
        Notification notification = extend(builder).build();
        // TODO(http://b/169394642): Undo this if/when the bug is fixed.
        notification.extras.putBundle(
                EXTRA_ONGOING_ACTIVITY,
                builder.getExtras().getBundle(EXTRA_ONGOING_ACTIVITY)
        );
        return notification;
    }

    @Nullable private static <T> T safeGetParcelableOrNull(@NonNull Bundle bundle,
            @NonNull String key, @NonNull Class<T> targetClass) {
        Parcelable obj = bundle.getParcelable(key);
        return targetClass.isInstance(obj) ? targetClass.cast(obj) : null;
    }

    /**
     * Deserializes the [OngoingActivityData] from a notification.
     */
    @Nullable
    @SuppressWarnings("SyntheticAccessor")
    static OngoingActivityData createInternal(@NonNull Notification notification) {
        Bundle bundle = notification.extras.getBundle(EXTRA_ONGOING_ACTIVITY);
        if (bundle != null) {
            OngoingActivityData data = new OngoingActivityData();
            data.mAnimatedIcon = safeGetParcelableOrNull(bundle, KEY_ANIMATED_ICON, Icon.class);
            data.mStaticIcon = safeGetParcelableOrNull(bundle, KEY_STATIC_ICON, Icon.class);
            data.mStatus = OngoingActivityStatus.create(bundle);
            data.mTouchIntent = safeGetParcelableOrNull(bundle, KEY_TOUCH_INTENT,
                    PendingIntent.class);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                data.mLocusId = Api29Impl.getLocusId(notification);
            }
            data.mOngoingActivityId = bundle.getInt(KEY_ID, DEFAULT_ID);
            return data;
        }
        return null;
    }

    /**
     * Deserializes the {@link OngoingActivityData} from a notification.
     *
     * Applies defaults from the notification for information not provided as part of the
     * {@link OngoingActivity}.
     *
     * @param notification the notification that may contain information about a Ongoing
     *                     Activity.
     * @return the data, or null of the notification doesn't contain Ongoing Activity data.
     */
    @Nullable
    public static OngoingActivityData create(@NonNull Notification notification) {
        OngoingActivityData data = createInternal(notification);
        if (data != null) {
            if (data.getAnimatedIcon() == null) {
                data.setAnimatedIcon(notification.getSmallIcon());
            }
            if (data.getStaticIcon() == null) {
                data.setStaticIcon(notification.getSmallIcon());
            }
            if (data.getStatus() == null) {
                String text = notification.extras.getString(Notification.EXTRA_TEXT);
                if (text != null) {
                    data.setStatus(new TextOngoingActivityStatus(text));
                }
            }
            if (data.getTouchIntent() == null) {
                data.setTouchIntent(notification.contentIntent);
            }
        }
        return data;
    }

    // Inner class required to avoid VFY errors during class init.
    @RequiresApi(29)
    private static class Api29Impl {
        // Avoid instantiation.
        private Api29Impl() {
        }

        @Nullable
        private static LocusIdCompat getLocusId(@NonNull Notification notification) {
            return notification.getLocusId() != null
                    ? LocusIdCompat.toLocusIdCompat(notification.getLocusId()) : null;
        }
    }

    /**
     * Get the animated icon that can be used on some surfaces to represent this
     * {@link OngoingActivity}. For example, in the WatchFace.
     */
    @Nullable
    public Icon getAnimatedIcon() {
        return mAnimatedIcon;
    }

    /**
     * Get the static icon that can be used on some surfaces to represent this
     * {@link OngoingActivity}. For example in the WatchFace in ambient mode.
     */
    @Nullable
    public Icon getStaticIcon() {
        return mStaticIcon;
    }

    /**
     * Get the status of this ongoing activity, the status may be displayed on the UI to
     * show progress of the Ongoing Activity.
     */
    @Nullable
    public OngoingActivityStatus getStatus() {
        return mStatus;
    }

    /**
     * Get the intent to be used to go back to the activity when the user interacts with the
     * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace)
     */
    @Nullable
    public PendingIntent getTouchIntent() {
        return mTouchIntent;
    }

    /**
     * Get the LocusId of this {@link OngoingActivity}, this can be used by the launcher to
     * identify the corresponding launcher item and display it accordingly.
     */
    @Nullable
    public LocusIdCompat getLocusId() {
        return mLocusId;
    }

    /**
     * Give the id to this {@link OngoingActivity}, as a way to reference it in
     * [fromExistingOngoingActivity]
     */
    public int getOngoingActivityId() {
        return mOngoingActivityId;
    }

    /* Package private setters */
    void setAnimatedIcon(@Nullable Icon animatedIcon) {
        this.mAnimatedIcon = animatedIcon;
    }

    void setStaticIcon(@Nullable Icon staticIcon) {
        this.mStaticIcon = staticIcon;
    }

    void setStatus(@Nullable OngoingActivityStatus status) {
        this.mStatus = status;
    }

    void setTouchIntent(@Nullable PendingIntent touchIntent) {
        this.mTouchIntent = touchIntent;
    }

    void setLocusId(@Nullable LocusIdCompat locusId) {
        this.mLocusId = locusId;
    }

    void setOngoingActivityId(int ongoingActivityId) {
        this.mOngoingActivityId = ongoingActivityId;
    }

    /** Notification action extra which contains ongoing activity extensions */
    private static final String EXTRA_ONGOING_ACTIVITY =
            "android.wearable.ongoingactivities.EXTENSIONS";

    // Keys within EXTRA_ONGOING_ACTIVITY_EXTENDER for ongoing activities options.
    private static final String KEY_ANIMATED_ICON = "animatedIcon";
    private static final String KEY_STATIC_ICON = "staticIcon";
    private static final String KEY_TOUCH_INTENT = "touchIntent";
    private static final String KEY_ID = "id";

    private static final int DEFAULT_ID = -1;
}