blob: ed556cdca2968e42f1f5e51286449f9cbc9b9373 [file] [log] [blame]
/*
* 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 partnerSummaryActionsCollector;
private final Context context;
private Uri mContentProviderUri;
/** private constructor, should use getter. */
private PartnerSummaryActionsCollector(Context context) {
this.context = 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 (partnerSummaryActionsCollector == null) {
partnerSummaryActionsCollector = new PartnerSummaryActionsCollector(context);
}
return partnerSummaryActionsCollector;
}
/**
* 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 = context.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 = context.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 = context.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 = context.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;
}
}