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.summary;
  
  import android.annotation.Nullable;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.net.Uri;
  import android.os.Bundle;
  import android.text.TextUtils;
  import android.util.Log;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.stream.Collectors;
  
  /**
   * Collects the actions provided by partners and compiles them into a list of {@link SummaryAction}
   * items.
   */
  public class PartnerSummaryActionsCollector {
  
      private static final String TAG = "PartnerSummaryActionsCollector";
  
      private static final String CONTENT_PROVIDER_INTENT_ACTION =
              "com.google.android.car.setupwizard.SETUP_ACTIONS_STATE_PROVIDER";
  
      // Methods for fetching info from the content provider.
      private static final String METHOD_GET_ACTION_COMPLETION_STATE = "get_action_completion_state";
      private static final String METHOD_GET_ACTION_SUMMARY_STATE = "get_action_summary_state";
      private static final String METHOD_GET_DEFERRED_ACTION_STATE =
              "get_deferred_action_state";
      private static final String METHOD_GET_SUMMARY_ACTIONS = "get_summary_actions";
  
      // Constants for fetching information from the bundles passed back by the content provider.
      private static final String EXTRA_SUMMARY_ACTIONS_LIST = "summary_actions_list";
      private static final String EXTRA_IS_ACTION_COMPLETED = "is_action_completed";
  
      // Constants for information contained within the summary action bundle.
      private static final String EXTRA_SUMMARY_ACTION_HAS_DEPENDENCY =
              "summary_action_has_dependency";
      private static final String EXTRA_SUMMARY_ACTION_DEPENDENCY_DESCRIPTION =
              "summary_action_dependency_description";
      private static final String EXTRA_SUMMARY_ACTION_TITLE = "summary_action_title";
      private static final String EXTRA_SUMMARY_ACTION_DESCRIPTION = "summary_action_description";
      private static final String EXTRA_SUMMARY_ACTION_REQUIRES_NETWORK =
              "summary_action_requires_network";
      private static final String EXTRA_SUMMARY_ACTION_WIZARD_SCRIPT = "summary_action_wizard_script";
      private static final String EXTRA_SUMMARY_ACTION_PRIORITY = "summary_action_priority";
      private static final String EXTRA_SUMMARY_ICON_RESOURCE_NAME =
              "summary_action_icon_resource_name";
      private static final String EXTRA_SUMMARY_COMPLETED_DESCRIPTION =
              "summary_action_completed_description";
      private static final String EXTRA_SUMMARY_ACTION_DEFERRED_NOTIFICATION_DESCRIPTION =
              "summary_action_deferred_notification_description";
  
      // Extra used as a key for the action id passed in to query summary action state.
      private static final String EXTRA_ACTION_ID = "action_id";
      private static PartnerSummaryActionsCollector sPartnerSummaryActionsCollector;
      private final Context mContext;
      private Uri mContentProviderUri;
  
      /** private constructor, should use getter. */
      private PartnerSummaryActionsCollector(Context context) {
          mContext = context;
          ResolveInfo resolveInfo = getSummaryContentProviderResolveInfo(context.getPackageManager());
  
          if (resolveInfo == null) {
              Log.e(TAG, "Could not find partner content provider, ignoring partner summary items.");
              return;
          }
  
          mContentProviderUri = getSummaryContentProviderUri(resolveInfo);
  
          if (mContentProviderUri == null) {
              Log.e(TAG, "Could not fetch content provider URI, ignoring partner summary items.");
          }
      }
  
      /** Gets the current instance of the {@link PartnerSummaryActionsCollector}. */
      public static PartnerSummaryActionsCollector get(Context context) {
          if (sPartnerSummaryActionsCollector == null) {
              sPartnerSummaryActionsCollector = new PartnerSummaryActionsCollector(context);
          }
          return sPartnerSummaryActionsCollector;
      }
  
      /**
       * Creates a summary action using the passed in completion state and summary state {@link
       * Bundle}.
       * This will pull out all relevant state such as title, description, dependencies, and anything
       * else that defines a summary item. Returns null if the bundle does not have all the required
       * state or is null.
       */
      @Nullable
      private static SummaryAction buildSummaryAction(
              boolean completed, Bundle summaryStateBundle) {
          if (summaryStateBundle == null) {
              return null;
          }
  
          String title = summaryStateBundle.getString(EXTRA_SUMMARY_ACTION_TITLE);
          if (title == null) {
              Log.e(TAG, "No title provided in summaryStateBundle: " + summaryStateBundle);
              return null;
          }
  
          String scriptUri = summaryStateBundle.getString(EXTRA_SUMMARY_ACTION_WIZARD_SCRIPT);
          if (scriptUri == null) {
              Log.e(TAG, "No wizard script provided in summaryStateBundle: " + summaryStateBundle);
              return null;
          }
  
          String description = summaryStateBundle.getString(EXTRA_SUMMARY_ACTION_DESCRIPTION, "");
          boolean requiresNetwork =
                  summaryStateBundle.getBoolean(EXTRA_SUMMARY_ACTION_REQUIRES_NETWORK, false);
          boolean hasUnfinishedDependency =
                  summaryStateBundle.getBoolean(EXTRA_SUMMARY_ACTION_HAS_DEPENDENCY, false);
          String unfinishedDependencyDescription = null;
          if (hasUnfinishedDependency) {
              unfinishedDependencyDescription =
                      summaryStateBundle.getString(EXTRA_SUMMARY_ACTION_DEPENDENCY_DESCRIPTION);
          }
          // Fetch priority, default 0 so that if no priority is provided they will be placed above
          // the Google items which are located in 100-200.
          int priority = summaryStateBundle.getInt(EXTRA_SUMMARY_ACTION_PRIORITY, 0);
          String iconResourceName = null;
          if (summaryStateBundle.containsKey(EXTRA_SUMMARY_ICON_RESOURCE_NAME)) {
              iconResourceName = summaryStateBundle.getString(EXTRA_SUMMARY_ICON_RESOURCE_NAME, "");
          }
  
          String completedDescription =
                  summaryStateBundle.getString(EXTRA_SUMMARY_COMPLETED_DESCRIPTION, description);
          return new SummaryAction(
                  title,
                  description,
                  requiresNetwork,
                  completed,
                  priority,
                  scriptUri,
                  hasUnfinishedDependency,
                  unfinishedDependencyDescription,
                  iconResourceName,
                  completedDescription);
      }
  
      /**
       * Creates a {@link DeferredAction} based on the passed in completion state and deferred action
       * state bundle. Will return null if there is no notification description or a null bundle.
       */
      private static DeferredAction buildDeferredAction(boolean completed,
              Bundle deferredActionState) {
          if (deferredActionState == null) {
              Log.e(TAG, "Cannot build deferred action with null deferredActionState");
              return null;
          }
  
          String deferredNotificationDescription = deferredActionState.getString(
                  EXTRA_SUMMARY_ACTION_DEFERRED_NOTIFICATION_DESCRIPTION);
          if (deferredNotificationDescription == null) {
              Log.v(TAG, "Cannot build deferred action with no notification description");
              return null;
          }
  
          int priority = deferredActionState.getInt(EXTRA_SUMMARY_ACTION_PRIORITY, 0);
          return new DeferredAction(deferredNotificationDescription, completed, priority);
      }
  
      private static ResolveInfo getSummaryContentProviderResolveInfo(PackageManager packageManager) {
          Intent contentProviderQueryIntent = new Intent(CONTENT_PROVIDER_INTENT_ACTION);
          List<ResolveInfo> queryResults =
                  packageManager.queryIntentContentProviders(contentProviderQueryIntent, 0);
          Log.v(TAG, "Query results size before pruning for system packages: " + queryResults.size());
          queryResults =
                  queryResults.stream()
                          .filter(
                                  resolveInfo ->
                                          resolveInfo.providerInfo != null
                                                  && resolveInfo.providerInfo.applicationInfo != null
                                                  && (resolveInfo.providerInfo.applicationInfo.flags
                                                  & ApplicationInfo.FLAG_SYSTEM)
                                                  != 0)
                          .collect(Collectors.toList());
          if (queryResults.size() > 1 || queryResults.isEmpty()) {
              Log.v(
                      TAG,
                      "Found "
                              + queryResults.size()
                              + " content providers, there should be exactly 1 to show partner "
                              + "actions. Ignoring"
                              + " all partner actions.");
              return null;
          }
          return queryResults.get(0);
      }
  
      private static Uri getSummaryContentProviderUri(ResolveInfo resolveInfo) {
          if (resolveInfo.providerInfo == null || TextUtils.isEmpty(
                  resolveInfo.providerInfo.authority)) {
              Log.e(TAG, "Incorrectly configured partner content provider");
              return null;
          }
          return new Uri.Builder()
                  .scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(resolveInfo.providerInfo.authority)
                  .build();
      }
  
      /**
       * Gets the list of provided partner summary actions. Will return an empty list if none are
       * found or there is an error loading them.
       */
      public List<SummaryAction> getPartnerSummaryActions() {
          if (mContentProviderUri == null) {
              Log.e(TAG, "No content provider URI found, summary actions ignored");
              return new ArrayList<>();
          }
          ArrayList<String> partnerSummaryActions;
          try {
              partnerSummaryActions = getPartnerSummaryActionsFromContentProvider(
                      mContentProviderUri);
          } catch (NullPointerException | IllegalArgumentException e) {
              Log.e(TAG, "Unable to find or successfully query content provider, ignoring action", e);
              return new ArrayList<>();
          }
  
          if (partnerSummaryActions == null || partnerSummaryActions.isEmpty()) {
              Log.v(TAG, "No actions were fetched for partners");
              return new ArrayList<>();
          }
  
          List<SummaryAction> summaryActionList = new ArrayList<>();
          for (String actionId : partnerSummaryActions) {
              Log.v(TAG, "Attempting to generate summary action for id: " + actionId);
              try {
                  boolean completed =
                          getActionCompletionStateFromContentProvider(actionId, mContentProviderUri);
                  Bundle summaryActionBundle =
                          getActionSummaryStateFromContentProvider(actionId, mContentProviderUri);
                  SummaryAction summaryAction = buildSummaryAction(completed, summaryActionBundle);
                  if (summaryAction != null) {
                      summaryActionList.add(summaryAction);
                  }
              } catch (NullPointerException | IllegalArgumentException e) {
                  Log.e(
                          TAG,
                          "Unable to load the completion or config state for summary action: "
                                  + actionId,
                          e);
              }
          }
          return summaryActionList;
      }
  
      /** Returns the set of partner deferred actions provided by the partner content provider. */
      public List<DeferredAction> getPartnerDeferredActions() {
          if (mContentProviderUri == null) {
              Log.e(TAG, "No content provider URI found, deferred actions ignored");
              return new ArrayList<>();
          }
  
          ArrayList<String> partnerSummaryActions;
          try {
              partnerSummaryActions = getPartnerSummaryActionsFromContentProvider(
                      mContentProviderUri);
          } catch (NullPointerException | IllegalArgumentException e) {
              Log.e(TAG, "Unable to find or successfully query content provider, ignoring action", e);
              return new ArrayList<>();
          }
  
          if (partnerSummaryActions == null || partnerSummaryActions.isEmpty()) {
              Log.v(TAG, "No actions were fetched for partners");
              return new ArrayList<>();
          }
  
          List<DeferredAction> deferredActions = new ArrayList<>();
          for (String actionId : partnerSummaryActions) {
              Log.v(TAG, "Attempting to generate deferred action for id: " + actionId);
              try {
                  boolean completed =
                          getActionCompletionStateFromContentProvider(actionId, mContentProviderUri);
                  Bundle deferredActionBundle =
                          getDeferredActionStateFromContentProvider(actionId, mContentProviderUri);
                  if (deferredActionBundle == null) {
                      Log.v(TAG, "No valid deferredActionBundle for action: " + actionId);
                      continue;
                  }
                  DeferredAction deferredAction = buildDeferredAction(completed,
                          deferredActionBundle);
                  if (deferredAction != null) {
                      deferredActions.add(deferredAction);
                  }
              } catch (NullPointerException e) {
                  Log.e(
                          TAG,
                          "Unable to load the completion or config state for deferred action: "
                                  + actionId,
                          e);
              }
          }
          return deferredActions;
      }
  
      /**
       * Gets the list of actionId's for the partner summary actions form the passed in content
       * provider
       * {@link Uri}.
       *
       * @throws NullPointerException     if the method is null on the content provider.
       * @throws IllegalArgumentException if uri is not known or the request method is not supported
       *                                  properly.
       */
      private ArrayList<String> getPartnerSummaryActionsFromContentProvider(Uri contentProviderUri) {
          Bundle result = mContext.getContentResolver().call(
                  contentProviderUri,
                  METHOD_GET_SUMMARY_ACTIONS,
                  /* arg= */ null,
                  /* extras= */ null);
          if (result == null || result.getStringArrayList(EXTRA_SUMMARY_ACTIONS_LIST) == null) {
              Log.e(
                      TAG,
                      "No summary actions returned from content resolve call, can't fetch partner "
                              + "actions.");
              throw new IllegalArgumentException(
                      "Uri: " + contentProviderUri + " did not return a list of actionId's.");
          }
          return result.getStringArrayList(EXTRA_SUMMARY_ACTIONS_LIST);
      }
  
      /**
       * Gets the completion state for the specific actionId passed in using the passed in content
       * provider {@link Uri}.
       *
       * @throws NullPointerException     if the method is null on the content provider.
       * @throws IllegalArgumentException if uri is not known or the request method is not supported
       *                                  properly.
       */
      private boolean getActionCompletionStateFromContentProvider(
              String actionId, Uri contentProviderUri) {
          Bundle completionStateArgs = new Bundle();
          completionStateArgs.putString(EXTRA_ACTION_ID, actionId);
          Bundle result = mContext.getContentResolver().call(
                  contentProviderUri,
                  METHOD_GET_ACTION_COMPLETION_STATE,
                  /* arg= */ null,
                  completionStateArgs);
          if (result == null || !result.containsKey(EXTRA_IS_ACTION_COMPLETED)) {
              throw new IllegalArgumentException(
                      "No action with id " + actionId + " found in content provider");
          }
          return result.getBoolean(EXTRA_IS_ACTION_COMPLETED, true);
      }
  
      private Bundle getActionSummaryStateFromContentProvider(String actionId,
              Uri contentProviderUri) {
          Bundle summaryStateArgs = new Bundle();
          summaryStateArgs.putString(EXTRA_ACTION_ID, actionId);
          Bundle result = mContext.getContentResolver().call(
                  contentProviderUri,
                  METHOD_GET_ACTION_SUMMARY_STATE,
                  /* arg= */ null,
                  summaryStateArgs);
          if (result == null) {
              throw new IllegalArgumentException(
                      "No action summary found in content provider for " + actionId);
          }
          return result;
      }
  
      private Bundle getDeferredActionStateFromContentProvider(String actionId,
              Uri contentProviderUri) {
          Bundle deferredStateArgs = new Bundle();
          deferredStateArgs.putString(EXTRA_ACTION_ID, actionId);
          Bundle result;
          try {
              result = mContext.getContentResolver().call(
                      contentProviderUri,
                      METHOD_GET_DEFERRED_ACTION_STATE,
                      /* arg= */ null,
                      deferredStateArgs);
          } catch (UnsupportedOperationException e) {
              Log.v(TAG, "Deferred notification query not supported by partner content provider");
              return null;
          }
          return result;
      }
  }
  