content: /*
   * Copyright (C) 2019 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.car.setupwizardlib.partner;
  
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.content.res.Resources;
  import android.content.res.Resources.NotFoundException;
  import android.graphics.drawable.Drawable;
  import android.net.Uri;
  import android.os.Bundle;
  import android.util.Log;
  import android.util.TypedValue;
  
  import androidx.annotation.ColorInt;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.VisibleForTesting;
  
  import java.util.EnumMap;
  
  /** The helper reads and caches the partner configurations from Car Setup Wizard. */
  public class PartnerConfigHelper {
  
      private static final String TAG = PartnerConfigHelper.class.getSimpleName();
  
      @VisibleForTesting
      static final String SUW_AUTHORITY = "com.google.android.car.setupwizard.partner";
  
      @VisibleForTesting
      static final String SUW_GET_PARTNER_CONFIG_METHOD = "getOverlayConfig";
      private static volatile PartnerConfigHelper sInstance = null;
  
      @VisibleForTesting Bundle mResultBundle = null;
  
      @VisibleForTesting
      final EnumMap<PartnerConfig, Object> mPartnerResourceCache = new EnumMap<>(PartnerConfig.class);
  
      /** Factory method to get an instance */
      public static PartnerConfigHelper get(@NonNull Context context) {
          if (sInstance == null) {
              synchronized (PartnerConfigHelper.class) {
                  if (sInstance == null) {
                      sInstance = new PartnerConfigHelper(context);
                  }
              }
          }
          return sInstance;
      }
  
      /**
       * Returns the color of given {@code partnerConfig}, or 0 if the given {@code partnerConfig}
       * is not found. If the {@code ResourceType} of the given {@code partnerConfig} is not color,
       * IllegalArgumentException will be thrown.
       *
       * @param context The context of client activity
       * @param partnerConfig The {@code PartnerConfig} of target resource
       */
      @ColorInt
      public int getColor(@NonNull Context context, PartnerConfig partnerConfig) {
          if (partnerConfig.getResourceType() != PartnerConfig.ResourceType.COLOR) {
              throw new IllegalArgumentException("Not a color resource");
          }
  
          if (mPartnerResourceCache.containsKey(partnerConfig)) {
              return (int) mPartnerResourceCache.get(partnerConfig);
          }
  
          int result = 0;
          try {
              String resourceName = partnerConfig.getResourceName();
              ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
              if (resourceEntry == null) {
                  Log.w(TAG, "Resource not found: " + resourceName);
                  return 0;
              }
  
              Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
              result = resource.getColor(resourceEntry.getResourceId(), null);
              mPartnerResourceCache.put(partnerConfig, result);
          } catch (PackageManager.NameNotFoundException exception) {
              Log.e(TAG, exception.getMessage());
          }
          return result;
      }
  
      /**
       * Returns the {@code Drawable} of given {@code partnerConfig}, or {@code null} if the given
       * {@code partnerConfig} is not found. If the {@code ResourceType} of the given {@code
       * resourceConfig} is not drawable, IllegalArgumentException will be thrown.
       *
       * @param context The context of client activity
       * @param partnerConfig The {@code PartnerConfig} of target resource
       */
      @Nullable
      public Drawable getDrawable(@NonNull Context context, PartnerConfig partnerConfig) {
          if (partnerConfig.getResourceType() != PartnerConfig.ResourceType.DRAWABLE) {
              throw new IllegalArgumentException("Not a drawable resource");
          }
  
          if (mPartnerResourceCache.containsKey(partnerConfig)) {
              return (Drawable) mPartnerResourceCache.get(partnerConfig);
          }
  
          Drawable result = null;
          try {
              String resourceName = partnerConfig.getResourceName();
              ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
              if (resourceEntry == null) {
                  Log.w(TAG, "Resource not found: " + resourceName);
                  return null;
              }
              Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
  
              // for @null
              TypedValue outValue = new TypedValue();
              resource.getValue(resourceEntry.getResourceId(), outValue, true);
              if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
                  return result;
              }
  
              result = resource.getDrawable(resourceEntry.getResourceId(), null);
              mPartnerResourceCache.put(partnerConfig, result);
          } catch (PackageManager.NameNotFoundException | NotFoundException exception) {
              Log.e(TAG, exception.getMessage());
          }
          return result;
      }
  
      /**
       * Returns the string of the given {@code partnerConfig}, or {@code null} if the given {@code
       * resourceConfig} is not found. If the {@code ResourceType} of the given {@code partnerConfig}
       * is not string, IllegalArgumentException will be thrown.
       *
       * @param context The context of client activity
       * @param partnerConfig The {@code PartnerConfig} of target resource
       */
      @Nullable
      public String getString(@NonNull Context context, PartnerConfig partnerConfig) {
          if (partnerConfig.getResourceType() != PartnerConfig.ResourceType.STRING) {
              throw new IllegalArgumentException("Not a string resource");
          }
  
          if (mPartnerResourceCache.containsKey(partnerConfig)) {
              return (String) mPartnerResourceCache.get(partnerConfig);
          }
  
          String result = null;
          try {
              String resourceName = partnerConfig.getResourceName();
              ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
              if (resourceEntry == null) {
                  Log.w(TAG, "Resource not found: " + resourceName);
                  return null;
              }
              Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
              result = resource.getString(resourceEntry.getResourceId());
              mPartnerResourceCache.put(partnerConfig, result);
          } catch (PackageManager.NameNotFoundException exception) {
              Log.e(TAG, exception.getMessage());
          }
          return result;
      }
  
      /**
       * Returns the dimension of given {@code partnerConfig}. The default return value is 0.
       *
       * @param context The context of client activity
       * @param resourceConfig The {@code PartnerConfig} of target resource
       */
      public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
          return getDimension(context, resourceConfig, 0);
      }
  
      /**
       * Returns the dimension of given {@code partnerConfig}. If the given {@code partnerConfig}
       * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
       * resourceConfig} is not dimension, will throw IllegalArgumentException.
       *
       * @param context The context of client activity
       * @param partnerConfig The {@code PartnerConfig} of target resource
       * @param defaultValue The default value
       */
      public float getDimension(
              @NonNull Context context, PartnerConfig partnerConfig, float defaultValue) {
          if (partnerConfig.getResourceType() != PartnerConfig.ResourceType.DIMENSION) {
              throw new IllegalArgumentException("Not a dimension resource");
          }
  
          if (mPartnerResourceCache.containsKey(partnerConfig)) {
              return (float) mPartnerResourceCache.get(partnerConfig);
          }
  
          float result = defaultValue;
          try {
              String resourceName = partnerConfig.getResourceName();
              ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
              if (resourceEntry == null) {
                  Log.w(TAG, "Resource not found: " + resourceName);
                  return defaultValue;
              }
              Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
              result = resource.getDimension(resourceEntry.getResourceId());
              mPartnerResourceCache.put(partnerConfig, result);
          } catch (PackageManager.NameNotFoundException exception) {
              Log.e(TAG, exception.getMessage());
          }
          return result;
      }
  
      /**
       * Returns the boolean value of given {@code partnerConfig}. If the given {@code partnerConfig}
       * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
       * resourceConfig} is not boolean, will throw IllegalArgumentException.
       *
       * @param context The context of client activity
       * @param partnerConfig The {@code PartnerConfig} of target resource
       * @param defaultValue The default value
       */
      public boolean getBoolean(
              @NonNull Context context, PartnerConfig partnerConfig, boolean defaultValue) {
          if (partnerConfig.getResourceType() != PartnerConfig.ResourceType.BOOLEAN) {
              throw new IllegalArgumentException("Not a boolean resource");
          }
  
          if (mPartnerResourceCache.containsKey(partnerConfig)) {
              return (boolean) mPartnerResourceCache.get(partnerConfig);
          }
  
          boolean result = defaultValue;
          try {
              String resourceName = partnerConfig.getResourceName();
              ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
              if (resourceEntry == null) {
                  Log.w(TAG, "Resource not found: " + resourceName);
                  return defaultValue;
              }
              Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
              result = resource.getBoolean(resourceEntry.getResourceId());
              mPartnerResourceCache.put(partnerConfig, result);
          } catch (PackageManager.NameNotFoundException exception) {
              Log.e(TAG, exception.getMessage());
          }
          return result;
      }
  
      private void getPartnerConfigBundle(Context context) {
          if (mResultBundle == null) {
              try {
                  Uri contentUri =
                          new Uri.Builder()
                                  .scheme(ContentResolver.SCHEME_CONTENT)
                                  .authority(SUW_AUTHORITY)
                                  .appendPath(SUW_GET_PARTNER_CONFIG_METHOD)
                                  .build();
                  mResultBundle = context.getContentResolver().call(
                          contentUri,
                          SUW_GET_PARTNER_CONFIG_METHOD,
                          /* arg= */ null,
                          /* extras= */ null);
                  mPartnerResourceCache.clear();
              } catch (IllegalArgumentException exception) {
                  Log.w(TAG, "Fail to get config from suw provider");
              }
          }
      }
  
      private Resources getResourcesByPackageName(Context context, String packageName)
              throws PackageManager.NameNotFoundException {
          PackageManager manager = context.getPackageManager();
          return manager.getResourcesForApplication(packageName);
      }
  
      private ResourceEntry getResourceEntryFromKey(String resourceName) {
          if (mResultBundle == null) {
              return null;
          }
          return ResourceEntry.fromBundle(mResultBundle.getBundle(resourceName));
      }
  
      private PartnerConfigHelper(Context context) {
          getPartnerConfigBundle(context);
      }
  
  
      @VisibleForTesting
      static synchronized void resetForTesting() {
          sInstance = null;
      }
  }
  