content: /*
   * Copyright (C) 2018 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.server.infra;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.StringRes;
  import android.annotation.UserIdInt;
  import android.content.Context;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.SystemClock;
  import android.text.TextUtils;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.SparseBooleanArray;
  import android.util.TimeUtils;
  
  import com.android.internal.annotations.GuardedBy;
  
  import java.io.PrintWriter;
  
  /**
   * Gets the service name using a framework resources, temporarily changing the service if necessary
   * (typically during CTS tests or service development).
   *
   * @hide
   */
  public final class FrameworkResourcesServiceNameResolver implements ServiceNameResolver {
  
      private static final String TAG = FrameworkResourcesServiceNameResolver.class.getSimpleName();
  
      /** Handler message to {@link #resetTemporaryService(int)} */
      private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
  
      private final @NonNull Context mContext;
      private final @NonNull Object mLock = new Object();
      private final @StringRes int mResourceId;
      private @Nullable NameResolverListener mOnSetCallback;
  
      /**
       * Map of temporary service name set by {@link #setTemporaryService(int, String, int)},
       * keyed by {@code userId}.
       *
       * <p>Typically used by Shell command and/or CTS tests.
       */
      @GuardedBy("mLock")
      private final SparseArray<String> mTemporaryServiceNames = new SparseArray<>();
  
      /**
       * Map of default services that have been disabled by
       * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}.
       *
       * <p>Typically used by Shell command and/or CTS tests.
       */
      @GuardedBy("mLock")
      private final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray();
  
      /**
       * When the temporary service will expire (and reset back to the default).
       */
      @GuardedBy("mLock")
      private long mTemporaryServiceExpiration;
  
      /**
       * Handler used to reset the temporary service name.
       */
      @GuardedBy("mLock")
      private Handler mTemporaryHandler;
  
      public FrameworkResourcesServiceNameResolver(@NonNull Context context,
              @StringRes int resourceId) {
          mContext = context;
          mResourceId = resourceId;
      }
  
      @Override
      public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) {
          synchronized (mLock) {
              this.mOnSetCallback = callback;
          }
      }
  
      @Override
      public String getDefaultServiceName(@UserIdInt int userId) {
          synchronized (mLock) {
              final String name = mContext.getString(mResourceId);
              return TextUtils.isEmpty(name) ? null : name;
          }
      }
  
      @Override
      public String getServiceName(@UserIdInt int userId) {
          synchronized (mLock) {
              final String temporaryName = mTemporaryServiceNames.get(userId);
              if (temporaryName != null) {
                  // Always log it, as it should only be used on CTS or during development
                  Slog.w(TAG, "getServiceName(): using temporary name " + temporaryName
                          + " for user " + userId);
                  return temporaryName;
              }
              final boolean disabled = mDefaultServicesDisabled.get(userId);
              if (disabled) {
                  // Always log it, as it should only be used on CTS or during development
                  Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for "
                          + "user " + userId);
                  return null;
              }
              return getDefaultServiceName(userId);
          }
      }
  
      @Override
      public boolean isTemporary(@UserIdInt int userId) {
          synchronized (mLock) {
              return mTemporaryServiceNames.get(userId) != null;
          }
      }
  
      @Override
      public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName,
              int durationMs) {
          synchronized (mLock) {
              mTemporaryServiceNames.put(userId, componentName);
  
              if (mTemporaryHandler == null) {
                  mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
                      @Override
                      public void handleMessage(Message msg) {
                          if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
                              synchronized (mLock) {
                                  resetTemporaryService(userId);
                              }
                          } else {
                              Slog.wtf(TAG, "invalid handler msg: " + msg);
                          }
                      }
                  };
              } else {
                  mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
              }
              mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs;
              mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
              notifyTemporaryServiceNameChangedLocked(userId, componentName,
                      /* isTemporary= */ true);
          }
      }
  
      @Override
      public void resetTemporaryService(@UserIdInt int userId) {
          synchronized (mLock) {
              Slog.i(TAG, "resetting temporary service for user " + userId + " from "
                      + mTemporaryServiceNames.get(userId));
              mTemporaryServiceNames.remove(userId);
              if (mTemporaryHandler != null) {
                  mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
                  mTemporaryHandler = null;
              }
              notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null,
                      /* isTemporary= */ false);
          }
      }
  
      @Override
      public boolean setDefaultServiceEnabled(int userId, boolean enabled) {
          synchronized (mLock) {
              final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId);
              if (currentlyEnabled == enabled) {
                  Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled);
                  return false;
              }
              if (enabled) {
                  Slog.i(TAG, "disabling default service for user " + userId);
                  mDefaultServicesDisabled.removeAt(userId);
              } else {
                  Slog.i(TAG, "enabling default service for user " + userId);
                  mDefaultServicesDisabled.put(userId, true);
              }
          }
          return true;
      }
  
      @Override
      public boolean isDefaultServiceEnabled(int userId) {
          synchronized (mLock) {
              return isDefaultServiceEnabledLocked(userId);
          }
      }
  
      private boolean isDefaultServiceEnabledLocked(int userId) {
          return !mDefaultServicesDisabled.get(userId);
      }
  
      @Override
      public String toString() {
          return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNames + "]";
      }
  
      // TODO(b/117779333): support proto
      @Override
      public void dumpShort(@NonNull PrintWriter pw) {
          synchronized (mLock) {
              pw.print("FrameworkResourcesServiceNamer: resId="); pw.print(mResourceId);
              pw.print(", numberTemps="); pw.print(mTemporaryServiceNames.size());
              pw.print(", enabledDefaults="); pw.print(mDefaultServicesDisabled.size());
          }
      }
  
      // TODO(b/117779333): support proto
      @Override
      public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) {
          synchronized (mLock) {
              final String temporaryName = mTemporaryServiceNames.get(userId);
              if (temporaryName != null) {
                  pw.print("tmpName="); pw.print(temporaryName);
                  final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime();
                  pw.print(" (expires in "); TimeUtils.formatDuration(ttl, pw); pw.print("), ");
              }
              pw.print("defaultName="); pw.print(getDefaultServiceName(userId));
              final boolean disabled = mDefaultServicesDisabled.get(userId);
              pw.println(disabled ? " (disabled)" : " (enabled)");
          }
      }
  
      private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId,
              @Nullable String newTemporaryName, boolean isTemporary) {
          if (mOnSetCallback != null) {
              mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary);
          }
      }
  }
  