/*
 * Copyright (C) 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 com.android.test.notificationtrampoline;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.ArraySet;

import androidx.annotation.Nullable;

import java.lang.ref.WeakReference;
import java.util.Set;
import java.util.stream.Stream;

/**
 * This is a bound service used in conjunction with trampoline tests in NotificationManagerTest.
 */
public class NotificationTrampolineTestService extends Service {
    private static final String TAG = "TrampolineTestService";
    private static final String NOTIFICATION_CHANNEL_ID = "cts/" + TAG;
    private static final String EXTRA_CALLBACK = "callback";
    private static final String EXTRA_ACTIVITY_REF = "activity_ref";
    private static final String RECEIVER_ACTION = ".TRAMPOLINE";
    private static final int MESSAGE_BROADCAST_NOTIFICATION = 1;
    private static final int MESSAGE_SERVICE_NOTIFICATION = 2;
    private static final int MESSAGE_CLICK_NOTIFICATION = 3;
    private static final int TEST_MESSAGE_BROADCAST_RECEIVED = 1;
    private static final int TEST_MESSAGE_SERVICE_STARTED = 2;
    private static final int TEST_MESSAGE_ACTIVITY_STARTED = 3;
    private static final int TEST_MESSAGE_NOTIFICATION_CLICKED = 4;
    private static final int PI_FLAGS =
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;

    private final Handler mHandler = new ServiceHandler();
    private final ActivityReference mActivityRef = new ActivityReference();
    private final Set<Integer> mPostedNotifications = new ArraySet<>();
    private NotificationManager mNotificationManager;
    private Messenger mMessenger;
    private BroadcastReceiver mReceiver;
    private Messenger mCallback;
    private String mReceiverAction;

    @Override
    public void onCreate() {
        mNotificationManager = getSystemService(NotificationManager.class);
        mMessenger = new Messenger(mHandler);
        mReceiverAction = getPackageName() + RECEIVER_ACTION;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    @Override
    public void onDestroy() {
        if (mReceiver != null) {
            unregisterReceiver(mReceiver);
        }
        WeakReference<Activity> activityRef = mActivityRef.activity;
        Activity activity = (activityRef != null) ? activityRef.get() : null;
        if (activity != null) {
            activity.finish();
        }
        for (int notificationId : mPostedNotifications) {
            mNotificationManager.cancel(notificationId);
        }
        mHandler.removeCallbacksAndMessages(null);
    }

    /** Suppressing since all messages are short-lived and we clear the queue on exit. */
    @SuppressLint("HandlerLeak")
    private class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message message) {
            Context context = NotificationTrampolineTestService.this;
            mCallback = (Messenger) message.obj;
            int notificationId = message.arg1;
            switch (message.what) {
                case MESSAGE_BROADCAST_NOTIFICATION: {
                    mReceiver = new BroadcastReceiver() {
                        @Override
                        public void onReceive(Context context, Intent broadcastIntent) {
                            sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED, true);
                            startTargetActivity();
                        }
                    };
                    registerReceiver(mReceiver, new IntentFilter(mReceiverAction));
                    Intent intent = new Intent(mReceiverAction);
                    postNotification(notificationId,
                            PendingIntent.getBroadcast(context, 0, intent, PI_FLAGS));
                    break;
                }
                case MESSAGE_SERVICE_NOTIFICATION: {
                    // We use this service to act as the trampoline since the bound lifecycle (which
                    // is as long as the test is being executed) outlives the started (used by the
                    // trampoline) in this case.
                    Intent intent = new Intent(context, NotificationTrampolineTestService.class);
                    postNotification(notificationId,
                            PendingIntent.getService(context, 0, intent, PI_FLAGS));
                    break;
                }
                case MESSAGE_CLICK_NOTIFICATION: {
                    PendingIntent intent = Stream
                            .of(mNotificationManager.getActiveNotifications())
                            .filter(sb -> sb.getId() == notificationId)
                            .map(sb -> sb.getNotification().contentIntent)
                            .findFirst()
                            .orElse(null);
                    if (intent != null) {
                        try {
                            intent.send();
                        } catch (PendingIntent.CanceledException e) {
                            throw new IllegalStateException("Notification PI cancelled", e);
                        }
                    }
                    sendMessageToTest(mCallback, TEST_MESSAGE_NOTIFICATION_CLICKED, intent != null);
                    break;
                }
                default:
                    throw new AssertionError("Unknown message " + message.what);
            }
        }
    }

    @Override
    public int onStartCommand(Intent serviceIntent, int flags, int startId) {
        sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED, true);
        startTargetActivity();
        stopSelf(startId);
        return START_REDELIVER_INTENT;
    }

    private void postNotification(int notificationId, PendingIntent intent) {
        Notification notification =
                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
                        .setSmallIcon(android.R.drawable.ic_info)
                        .setContentIntent(intent)
                        .build();
        NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
        mNotificationManager.createNotificationChannel(notificationChannel);
        mNotificationManager.notify(notificationId, notification);
        mPostedNotifications.add(notificationId);
    }

    private void startTargetActivity() {
        Intent intent = new Intent(this, TargetActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Bundle extras = new Bundle();
        extras.putParcelable(EXTRA_CALLBACK, mCallback);
        extras.putBinder(EXTRA_ACTIVITY_REF, mActivityRef);
        intent.putExtras(extras);
        startActivity(intent);
    }

    private static void sendMessageToTest(Messenger callback, int message, boolean success) {
        try {
            callback.send(Message.obtain(null, message, success ? 0 : 1, 0));
        } catch (RemoteException e) {
            throw new IllegalStateException(
                    "Couldn't send message " + message + " to test process", e);
        }
    }

    /**
     * A holder object that extends from Binder just so I can send it around using startActivity()
     * and avoid using static state. Works since the communication is local.
     */
    private static class ActivityReference extends Binder {
        public WeakReference<Activity> activity;
    }

    public static class TargetActivity extends Activity {
        @Override
        protected void onResume() {
            super.onResume();
            Messenger callback = getIntent().getParcelableExtra(EXTRA_CALLBACK);
            IBinder activityRef = getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF);
            if (activityRef instanceof ActivityReference) {
                ((ActivityReference) activityRef).activity = new WeakReference<>(this);
            }
            sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED, true);
        }
    }
}
