content: package org.lsposed.lspd.service;
  
  import static org.lsposed.lspd.service.ServiceManager.TAG;
  
  import android.app.INotificationManager;
  import android.app.Notification;
  import android.app.NotificationChannel;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.ParceledListSlice;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.drawable.AdaptiveIconDrawable;
  import android.graphics.drawable.BitmapDrawable;
  import android.graphics.drawable.Drawable;
  import android.graphics.drawable.Icon;
  import android.graphics.drawable.LayerDrawable;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.util.Log;
  
  import org.lsposed.daemon.R;
  import org.lsposed.lspd.util.FakeContext;
  
  import java.util.ArrayList;
  import java.util.Map;
  import java.util.UUID;
  import java.util.concurrent.ConcurrentHashMap;
  
  import io.github.libxposed.service.IXposedScopeCallback;
  
  public class LSPNotificationManager {
      static final String UPDATED_CHANNEL_ID = "lsposed_module_updated";
      static final String SCOPE_CHANNEL_ID = "lsposed_module_scope";
      private static final String STATUS_CHANNEL_ID = "lsposed_status";
      private static final int STATUS_NOTIFICATION_ID = 2000;
      private static final String opPkg = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
              "android" : "com.android.settings";
  
      private static final Map<String, Integer> notificationIds = new ConcurrentHashMap<>();
      private static int previousNotificationId = STATUS_NOTIFICATION_ID;
  
      static final String openManagerAction = UUID.randomUUID().toString();
      static final String moduleScope = UUID.randomUUID().toString();
  
      private static INotificationManager notificationManager = null;
      private static IBinder binder = null;
  
      private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() {
          @Override
          public void binderDied() {
              Log.w(TAG, "notificationManager is dead");
              binder.unlinkToDeath(this, 0);
              binder = null;
              notificationManager = null;
          }
      };
  
      private static INotificationManager getNotificationManager() throws RemoteException {
          if (binder == null || notificationManager == null) {
              binder = android.os.ServiceManager.getService(Context.NOTIFICATION_SERVICE);
              binder.linkToDeath(recipient, 0);
              notificationManager = INotificationManager.Stub.asInterface(binder);
          }
          return notificationManager;
      }
  
      private static Bitmap getBitmap(int id) {
          var r = ConfigFileManager.getResources();
          var res = r.getDrawable(id, r.newTheme());
          if (res instanceof BitmapDrawable) {
              return ((BitmapDrawable) res).getBitmap();
          } else {
              if (res instanceof AdaptiveIconDrawable) {
                  var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(),
                          ((AdaptiveIconDrawable) res).getForeground()};
                  res = new LayerDrawable(layers);
              }
              var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(),
                      res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
              var canvas = new Canvas(bitmap);
              res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
              res.draw(canvas);
              return bitmap;
          }
      }
  
      private static Icon getNotificationIcon() {
          return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification));
      }
  
      private static boolean hasNotificationChannelForSystem(
              INotificationManager nm, String channelId) throws RemoteException {
          NotificationChannel channel;
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
              channel = nm.getNotificationChannelForPackage("android", 1000, channelId, null, false);
          } else {
              channel = nm.getNotificationChannelForPackage("android", 1000, channelId, false);
          }
          if (channel != null) {
              Log.d(TAG, "hasNotificationChannelForSystem: " + channel);
          }
          return channel != null;
      }
  
      private static void createNotificationChannel(INotificationManager nm) throws RemoteException {
          var context = new FakeContext();
          var list = new ArrayList<NotificationChannel>();
  
          var updated = new NotificationChannel(UPDATED_CHANNEL_ID,
                  context.getString(R.string.module_updated_channel_name),
                  NotificationManager.IMPORTANCE_HIGH);
          updated.setShowBadge(false);
          if (hasNotificationChannelForSystem(nm, UPDATED_CHANNEL_ID)) {
              Log.d(TAG, "update notification channel: " + UPDATED_CHANNEL_ID);
              nm.updateNotificationChannelForPackage("android", 1000, updated);
          } else {
              list.add(updated);
          }
  
          var status = new NotificationChannel(STATUS_CHANNEL_ID,
                  context.getString(R.string.status_channel_name),
                  NotificationManager.IMPORTANCE_MIN);
          status.setShowBadge(false);
          if (hasNotificationChannelForSystem(nm, STATUS_CHANNEL_ID)) {
              Log.d(TAG, "update notification channel: " + STATUS_CHANNEL_ID);
              nm.updateNotificationChannelForPackage("android", 1000, status);
          } else {
              list.add(status);
          }
  
          var scope = new NotificationChannel(SCOPE_CHANNEL_ID,
                  context.getString(R.string.scope_channel_name),
                  NotificationManager.IMPORTANCE_HIGH);
          scope.setShowBadge(false);
          if (hasNotificationChannelForSystem(nm, SCOPE_CHANNEL_ID)) {
              Log.d(TAG, "update notification channel: " + SCOPE_CHANNEL_ID);
              nm.updateNotificationChannelForPackage("android", 1000, scope);
          } else {
              list.add(scope);
          }
  
          Log.d(TAG, "create notification channels for android: " + list);
          nm.createNotificationChannelsForPackage("android", 1000, new ParceledListSlice<>(list));
      }
  
      static void notifyStatusNotification() {
          var intent = new Intent(openManagerAction);
          intent.setPackage("android");
          var context = new FakeContext();
          int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
          var notification = new Notification.Builder(context, STATUS_CHANNEL_ID)
                  .setContentTitle(context.getString(R.string.lsposed_running_notification_title))
                  .setContentText(context.getString(R.string.lsposed_running_notification_content))
                  .setSmallIcon(getNotificationIcon())
                  .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags))
                  .setVisibility(Notification.VISIBILITY_SECRET)
                  .setColor(0xFFF48FB1)
                  .setOngoing(true)
                  .setAutoCancel(false)
                  .build();
          notification.extras.putString("android.substName", "LSPosed");
          try {
              var nm = getNotificationManager();
              createNotificationChannel(nm);
              nm.enqueueNotificationWithTag("android", opPkg, null,
                      STATUS_NOTIFICATION_ID, notification, 0);
          } catch (RemoteException e) {
              Log.e(TAG, "notifyStatusNotification: ", e);
          }
      }
  
      static void cancelStatusNotification() {
          try {
              var nm = getNotificationManager();
              createNotificationChannel(nm);
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                  nm.cancelNotificationWithTag("android", "android", null, STATUS_NOTIFICATION_ID, 0);
              } else {
                  nm.cancelNotificationWithTag("android", null, STATUS_NOTIFICATION_ID, 0);
              }
          } catch (RemoteException e) {
              Log.e(TAG, "cancelStatusNotification: ", e);
          }
      }
  
      private static PendingIntent getModuleIntent(String modulePackageName, int moduleUserId) {
          var intent = new Intent(openManagerAction);
          intent.setPackage("android");
          intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build());
          int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
          return PendingIntent.getBroadcast(new FakeContext(), 3, intent, flags);
      }
  
      private static PendingIntent getModuleScopeIntent(String modulePackageName, int moduleUserId, String scopePackageName, String action, IXposedScopeCallback callback) {
          var intent = new Intent(moduleScope);
          intent.setPackage("android");
          intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).encodedPath(scopePackageName).appendQueryParameter("action", action).build());
          var extras = new Bundle();
          extras.putBinder("callback", callback.asBinder());
          intent.putExtras(extras);
          int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
          return PendingIntent.getBroadcast(new FakeContext(), 4, intent, flags);
      }
  
      private static String getNotificationIdKey(String channel, String modulePackageName, int moduleUserId) {
          return channel + "/" + modulePackageName + ":" + moduleUserId;
      }
  
      private static int pushAndGetNotificationId(String channel, String modulePackageName, int moduleUserId) {
          var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId);
          // previousNotificationId start with 2001
          // https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto
          // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h
          // (AID_APP_END - AID_APP_START) x10 = 100000 < NOTE_NETWORK_AVAILABLE
          return notificationIds.computeIfAbsent(idKey, key -> previousNotificationId++);
      }
  
      static void notifyModuleUpdated(String modulePackageName,
                                      int moduleUserId,
                                      boolean enabled,
                                      boolean systemModule) {
          var context = new FakeContext();
          var userName = UserService.getUserName(moduleUserId);
          String title = context.getString(enabled ? systemModule ?
                  R.string.xposed_module_updated_notification_title_system :
                  R.string.xposed_module_updated_notification_title :
                  R.string.module_is_not_activated_yet);
          String content = context.getString(enabled ? systemModule ?
                  R.string.xposed_module_updated_notification_content_system :
                  R.string.xposed_module_updated_notification_content :
                  (moduleUserId == 0 ?
                          R.string.module_is_not_activated_yet_main_user_detailed :
                          R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName);
  
          var style = new Notification.BigTextStyle();
          style.bigText(content);
  
          var notification = new Notification.Builder(context, UPDATED_CHANNEL_ID)
                  .setContentTitle(title)
                  .setContentText(content)
                  .setSmallIcon(getNotificationIcon())
                  .setContentIntent(getModuleIntent(modulePackageName, moduleUserId))
                  .setVisibility(Notification.VISIBILITY_SECRET)
                  .setColor(0xFFF48FB1)
                  .setAutoCancel(true)
                  .setStyle(style)
                  .build();
          notification.extras.putString("android.substName", "LSPosed");
          try {
              var nm = getNotificationManager();
              nm.enqueueNotificationWithTag("android", opPkg, modulePackageName,
                      pushAndGetNotificationId(UPDATED_CHANNEL_ID, modulePackageName, moduleUserId),
                      notification, 0);
          } catch (RemoteException e) {
              Log.e(TAG, "notify module updated", e);
          }
      }
  
      static void requestModuleScope(String modulePackageName, int moduleUserId, String scopePackageName, IXposedScopeCallback callback) {
          var context = new FakeContext();
          var userName = UserService.getUserName(moduleUserId);
          String title = context.getString(R.string.xposed_module_request_scope_title);
          String content = context.getString(R.string.xposed_module_request_scope_content, modulePackageName, userName, scopePackageName);
  
          var style = new Notification.BigTextStyle();
          style.bigText(content);
  
          var notification = new Notification.Builder(context, SCOPE_CHANNEL_ID)
                  .setContentTitle(title)
                  .setContentText(content)
                  .setSmallIcon(getNotificationIcon())
                  .setVisibility(Notification.VISIBILITY_SECRET)
                  .setColor(0xFFF48FB1)
                  .setAutoCancel(true)
                  .setTimeoutAfter(1000 * 60 * 60)
                  .setStyle(style)
                  .setDeleteIntent(getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "delete", callback))
                  .setActions(new Notification.Action.Builder(
                                  Icon.createWithResource(context, R.drawable.ic_baseline_check_24),
                                  context.getString(R.string.scope_approve),
                                  getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "approve", callback))
                                  .build(),
                          new Notification.Action.Builder(
                                  Icon.createWithResource(context, R.drawable.ic_baseline_close_24),
                                  context.getString(R.string.scope_deny),
                                  getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "deny", callback))
                                  .build(),
                          new Notification.Action.Builder(
                                  Icon.createWithResource(context, R.drawable.ic_baseline_block_24),
                                  context.getString(R.string.nerver_ask_again),
                                  getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback))
                                  .build()
                  ).build();
          notification.extras.putString("android.substName", "LSPosed");
          try {
              var nm = getNotificationManager();
              nm.enqueueNotificationWithTag("android", opPkg, modulePackageName,
                      pushAndGetNotificationId(SCOPE_CHANNEL_ID, modulePackageName, moduleUserId),
                      notification, 0);
          } catch (RemoteException e) {
              try {
                  callback.onScopeRequestFailed(scopePackageName, e.getMessage());
              } catch (RemoteException ignored) {
              }
              Log.e(TAG, "request module scope", e);
          }
      }
  
      static void cancelNotification(String channel, String modulePackageName, int moduleUserId) {
          try {
              var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId);
              var idValue = notificationIds.get(idKey);
              if (idValue == null) return;
              var nm = getNotificationManager();
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                  nm.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0);
              } else {
                  nm.cancelNotificationWithTag("android", modulePackageName, idValue, 0);
              }
              notificationIds.remove(idKey);
          } catch (RemoteException e) {
              Log.e(TAG, "cancel notification", e);
          }
      }
  }
  